Portabilità (cambio di PA)
Un contribuente può cambiare Plateforme Agréée in qualsiasi momento, e nel farlo conserva il proprio indirizzamento basato su SIREN/SIRET — l'annuario del PPF garantisce la portabilità dell'identificativo, quindi nulla a valle deve essere reindirizzato. Ciò che deve avvenire, invece, è un passaggio di consegne fra le due piattaforme: la PA entrante chiede, la PA uscente risponde, e l'annuario viene commutato a una data di efficacia concordata.
Questa guida copre entrambe le direzioni, perché Flowie svolge entrambi i ruoli: entrante (un contribuente ha scelto Flowie, siamo noi a emettere la richiesta) e uscente (un'altra PA porta via un contribuente, e dobbiamo rispondere entro il termine di legge). Quattro endpoint coprono lo scambio — due per l'onboarding della società, due per parlare il formato di trasmissione inter-PA.
I termini da rispettare
La portabilità è un problema di scadenze prima ancora che di integrazione. Tre regole governano tutto quello che segue:
- Riscontrare entro 24 ore dalla ricezione della richiesta.
- Decidere entro 5 giorni lavorativi: i fine settimana e i giorni festivi non contano.
- Il silenzio vale assenso: scaduto il termine, il passaggio procede senza l'approvazione della piattaforma uscente.
Un riscontro mancato è di per sé una non conformità, a prescindere da quale sarebbe stata
la decisione. Marcate temporalmente ogni messaggio inviato e ricevuto — la colonna
request_datetime del CSV più sotto esiste esattamente per quella prova.
1 · Importare il contribuente dal suo SIRET
Il flusso del portale dà un solo dato in ingresso: il SIRET del contribuente. Tutto il
resto ne deriva. Flowie ricava il SIREN dalle prime nove cifre, implica il paese FR, costruisce
l'identificativo Peppol 0009:<siren> e risolve la denominazione legale — e la PA attuale
— dall'annuario del PPF.
curl -X POST …/v1/companies/import \
-H "Authorization: Bearer $FLOWIE_KEY" \
-H "Content-Type: application/json" \
-d '{
"siret": "92137626500017",
"mode": "portability"
}'
Tre esiti, in ordine di priorità:
- Import — avete passato un
sovosCompanyId: la società esistente viene recuperata e il suo codice fiscale, il nome e le capacità fanno fede. - Provisioning — nessun id società ma un'organizzazione nota (campo della richiesta o valore predefinito configurato): viene creata una connessione gestita a partire dal SIRET.
- Solo locale — né l'uno né l'altro: la società viene registrata
come
pending_verificatione viene scritto un eventocompany.import.pendingperché il team operativo possa collegarla in seguito.
L'onboarding non fallisce mai in modo netto per la mancanza di un id di backend, il che conta quando una richiesta di passaggio arriva prima che la parte contrattuale sia chiusa. La chiamata è idempotente sul SIRET: rieseguirla risincronizza la registrazione esistente invece di crearne una seconda, quindi un retry dopo un timeout è sicuro.
In risposta ottenete l'organizzazione con il suo id e il suo peppolId. Valorizzate
companyName o countryCode solo per sovrascrivere quanto risolve l'annuario —
ometteteli e vince l'annuario.
2 · Importare in blocco
Una migrazione di piattaforma sposta centinaia di società in una volta sola.
POST /v1/companies/import/batch accetta un elenco degli stessi oggetti e li elabora in
concorrenza, cinque alla volta.
curl -X POST …/v1/companies/import/batch \
-H "Authorization: Bearer $FLOWIE_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{"siret": "92137626500017"},
{"siret": "55208131766522"}
]
}'
La risposta contiene una riga di risultato per ogni elemento, nell'ordine di ingresso:
{
"results": [
{"index": 0, "status": "imported", "companyId": "org_…", "peppolId": "0009:921376265"},
{"index": 1, "status": "failed", "error": "siret must be 14 digits"}
]
}
Un elemento fallito non affonda il lotto. La chiamata restituisce comunque 200 con
un risultato parziale: controllate ogni riga anziché il codice di stato — index rimanda
alla posizione nella vostra richiesta. Poiché ogni elemento segue lo stesso percorso idempotente, rinviare
l'intero lotto per ritentare i falliti non duplicherà quelli già andati a buon fine.
3 · Inviare il messaggio inter-PA
Il canale che l'AIFE impone fra piattaforme è l'e-mail, con un oggetto normalizzato, uno
stato codificato e un CSV a 18 campi. POST /v1/portability/messages assembla quel messaggio, lo
invia per e-mail alla piattaforma controparte e lo registra — così non dovete mai
formattare un oggetto a mano, né cercare dove spedirlo, né perdere la prova di averlo spedito.
curl -X POST …/v1/portability/messages \
-H "Authorization: Bearer $FLOWIE_KEY" \
-H "Content-Type: application/json" \
-d '{
"messageType": "REQUEST",
"state": "received",
"requestRef": "POR-2026-000123",
"directionRole": "GAINING_PA",
"taxpayerSiren": "921376265",
"taxpayerSiret": "92137626500017",
"losingPaName": "ESKER",
"effectiveDate": "2026-09-01",
"mandateRef": "MDT-2026-8891"
}'
Ottenete l'identificativo di registro, l'oggetto, il CSV come intestazione più riga e la sua impronta, a chi è stato indirizzato e come quell'indirizzo è stato trovato, e se è davvero partito:
{
"id": "pmsg_9f2c7a1d4b8e4c0f9a6d3e2b1c7f5a80",
"subject": "[PORTABILITE][REQUEST][REQ][SIREN:921376265][REF:POR-2026-000123]",
"messageType": "REQUEST",
"statusCode": "REQ",
"state": "received",
"filename": "POR-2026-000123-REQ.csv",
"csvHeader": "request_ref;message_type;…",
"csvRow": "POR-2026-000123;REQUEST;REQ;…",
"csvSha256": "6b1f…",
"to": "[email protected]",
"recipientSource": "registry:ESKER",
"dispatched": true,
"reason": "sent",
"smtpMessageId": "<176…@flowie.fr>",
"createdAt": "2026-09-01T08:14:02.114000+00:00"
}
losingPaName quando siete la piattaforma entrante,
gainingPaName quando siete quella uscente — e viene risolta sulla
rubrica delle Plateformes Agréées registrate: la casella dedicata alla
portabilità che la piattaforma ha pubblicato, se ne ha una, altrimenti il suo courriel de contact
DGFiP. recipientSource dice cosa è successo (registry:<nome>,
explicit se avete passato to voi, oppure unresolved se il nome non
corrisponde a nulla). to vince sempre.
dispatched è false e
reason dice perché — dispatch_disabled, not_configured (nessun
relay o nessun mittente), no_recipient (nessun indirizzo a cui spedirlo), sandbox, o
l'errore SMTP stesso. La riga nel registro viene scritta comunque: un ambiente a secco produce la stessa pista di
audit senza l'e-mail, e un guasto del relay vi lascia il messaggio esatto da rispedire invece di un buco. Un ambiente
non di produzione può anche forzare un destinatario di ripiego: il messaggio risolve e registra la vera
controparte, ma viene recapitato all'indirizzo di ripiego — provare non scrive mai a una piattaforma reale.
PORTABILITY_CHANNEL_FROM, che il corpo indica. Quel circuito allega i file per
riferimento, quindi il CSV viaggia in linea nel corpo — identico byte per byte al
csvRow restituito e a ciò che è stato calcolato in hash. Se vi serve il CSV come vero
file .csv, forzate il relay diretto (PORTABILITY_TRANSPORT=smtp).
transport, nella risposta e nel registro, dice quale dei due lo ha portato.
I quattro valori di messageType corrispondono alle fasi dello scambio — REQUEST,
ACK, DECISION, COMPLETION — mentre state è il vostro
stato interno, tradotto per voi nel codice di stato di trasmissione (vedi Stati della
richiesta). directionRole dice chi sta parlando: GAINING_PA o
LOSING_PA.
La grammatica dell'oggetto è rigida e posizionale:
[PORTABILITE][<MESSAGE_TYPE>][<STATUS_CODE>][SIREN:<9 cifre>][REF:<request_ref>]
Un SIREN che non ha esattamente nove cifre, un tipo di messaggio sconosciuto o un requestRef che
contiene ] viene respinto con 400 prima che venga costruito alcunché.
4 · Analizzare un messaggio in entrata
L'altra metà: riportare un messaggio ricevuto a campi strutturati. Passate l'oggetto e, quando l'avete, la riga CSV.
curl -X POST …/v1/portability/messages/parse \
-H "Authorization: Bearer $FLOWIE_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "[PORTABILITE][DECISION][ACC][SIREN:921376265][REF:POR-2026-000123]",
"csvRow": "POR-2026-000123;DECISION;ACC;LOSING_PA;921376265;…"
}'
La risposta restituisce il tipo di messaggio, il codice di stato di trasmissione, lo state interno
a cui corrisponde, il SIREN, il riferimento della richiesta e — se è stata fornita una riga — le
18 colonne analizzate in fields.
Un oggetto che non rispetta la grammatica restituisce 400: mettetelo in coda di scarto, non
apriteci sopra una richiesta. È tutto il senso di un oggetto normalizzato — ciò che non
si analizza non è un messaggio di portabilità, e indovinarne l'intento è il modo migliore per
far migrare il contribuente sbagliato. Una riga CSV che non ha esattamente 18 colonne viene respinta allo stesso
modo.
5 · Commutare l'instradamento alla data di efficacia
Concordare un passaggio non cambia nulla di per sé. Ciò che decide dove va una fattura è
l'indirizzo di fatturazione elettronica del soggetto sul backend di conformità — è quello che
la directory finisce per instradare — e POST /v1/portability/routing è ciò che lo
sposta:
curl -X POST …/v1/portability/routing \
-H "Authorization: Bearer $FLOWIE_KEY" \
-H "Content-Type: application/json" \
-d '{
"organizationId": "019c76b2-9c94-7000-8cb6-ef104afb6093",
"siren": "921376265",
"siret": "92137626500018",
"effectiveDate": "2026-10-01",
"role": "GAINING_PA"
}'
{
"organizationId": "019c76b2-9c94-7000-8cb6-ef104afb6093",
"connectionId": "conn_7Yb3…",
"role": "GAINING_PA",
"siren": "921376265",
"effectiveDate": "2026-10-01",
"serviceUntil": null,
"created": true,
"address": { "id": "addr_2Kd9…", "siren": "921376265", "active": true }
}
La data è il punto. Come piattaforma entrante dichiarate l'indirizzo con
validFrom = la data di efficacia: un passaggio concordato ad agosto per il 1° ottobre non inizia a
tirare fatture ad agosto. Come piattaforma uscente (role: "LOSING_PA") non viene
cancellato nulla: l'emissione si ferma alla data di efficacia mentre la ricezione resta aperta fino a
data di efficacia + 12 mesi — il servizio minimo che la LFI 2026 impone alla piattaforma
uscente, perché i flussi già in corso si concludano. Modificate la finestra con
minimalServiceMonths se un contratto promette di più.
È idempotente sul SIREN: un indirizzo già dichiarato per esso viene aggiornato, mai duplicato. Se
l'organizzazione ha più di una connessione dovete indicarla con connectionId — sceglierne
una al posto vostro è il modo in cui un passaggio finisce sulla società sbagliata. E se state
inserendo il soggetto nello stesso movimento, POST /v1/companies/import accetta ora la stessa
effectiveDate e la riporta sull'indirizzo che crea.
La rubrica delle PA
Una richiesta di passaggio vale quanto ciò che sapete di dove l'altra piattaforma legge la posta.
GET /v1/portability/platforms è quella rubrica — tutti gli operatori registrati dalla
DGFiP, con l'indirizzo a cui un messaggio di portabilità deve davvero andare:
curl "…/v1/portability/platforms?q=esker" \
-H "Authorization: Bearer $FLOWIE_KEY"
{
"data": [
{
"name": "ESKER",
"website": "https://www.esker.fr/",
"email": "[email protected]",
"portabilityEmail": "[email protected]",
"contactEmail": "[email protected]",
"registeredOn": "2025-10-14",
"status": "registered"
}
],
"total": 1,
"source": "https://www.impots.gouv.fr/je-consulte-la-liste-des-plateformes-agreees",
"snapshotDate": "2026-08-20"
}
Unisce i due elenchi ufficiali della DGFiP — gli operatori che soddisfano ogni condizione
(status: "registered") e quelli ancora in attesa dei test di interoperabilità
("pending_interop") — con le caselle di portabilità dedicate che le piattaforme si sono
scambiate fra loro. contactEmail è quello che conta: l'indirizzo dedicato quando c'è, il
contatto DGFiP generico altrimenti. Filtrate con q (nome, e-mail o sito) e status.
L'elenco cambia ogni settimana con le nuove registrazioni, quindi la risposta porta con sé la propria
snapshotDate e la source da cui è costruita. Se vi serve l'elenco che fa fede in
questo istante, quella fonte è l'elenco.
Stati della richiesta
Una richiesta di portabilità attraversa otto stati. Ognuno corrisponde a un codice breve trasportato dall'oggetto e dal CSV, così che una controparte possa instradarlo automaticamente:
| Stato | Codice | Significato |
|---|---|---|
received | REQ | Richiesta creata o messaggio in entrata analizzato. Avvia i termini di 24 h e 5 giorni. |
acknowledged | ACK | Ricezione confermata entro 24 h. Il primo termine è rispettato. |
accepted | ACC | Decisione favorevole, entro la finestra di 5 giorni. |
rejected | REJ | Rifiuto. È atteso un motivo — valorizzate reasonCode e reasonText. |
auto_accepted | TAC | Assenso tacito: il termine è scaduto senza decisione, quindi il silenzio vale assenso. |
executing | MIG | Data di efficacia raggiunta; la commutazione dell'annuario è in corso. |
completed | CMP | L'annuario conferma che la nuova piattaforma è attiva. |
failed | ERR | Errore di trasporto o di annuario. |
Inviate lo stato interno in state; il codice di trasmissione non lo scrivete mai voi. Al ritorno,
parse risolve il codice nello stato per voi. Uno stato sconosciuto dà un 400, non
un passaggio silenzioso.
I due stati di decisione si distinguono per chi li innesca. accepted e rejected sono
un atto deliberato entro la finestra; auto_accepted è ciò che accade a chi
è rimasto in silenzio quando la finestra si chiude. Se siete la piattaforma uscente, un TAC
sulla vostra richiesta è il segnale che avete mancato la scadenza.
Tracciare una richiesta
L'onboarding della società scrive nel registro eventi, ed è lì che oggi si segue una migrazione:
curl "…/v1/events?type=company.imported&limit=100" \
-H "Authorization: Bearer $FLOWIE_KEY"
Il percorso di import scrive due tipi di evento: company.imported quando la registrazione è
attiva e company.import.pending quando la società è rimasta solo locale e attende un
collegamento operativo. Ciascuno porta l'id della società, l'identificativo Peppol, il SIRET, il SIREN e il
paese, il che basta a riconciliare una migrazione di massa riga per riga. L'elenco è paginato a cursore
— continuate a passare il cursor restituito finché hasMore non è
false.
GET /v1/events ma non figurano nel
catalogo dei webhook: sottoscrivere un webhook a company.imported non consegnerà nulla. Per ora
interrogate il registro; la reference dei webhook elenca ciò
che viene effettivamente inviato.
Ogni messaggio inviato viene registrato, e il registro si interroga:
curl "…/v1/portability/messages?requestRef=POR-2026-000123" \
-H "Authorization: Bearer $FLOWIE_KEY"
GET /v1/portability/messages li elenca dal più recente, limitati alla vostra organizzazione,
filtrabili per requestRef, siren, state, messageType e
dispatched — così un riferimento ripercorre un intero scambio, e
dispatched=false trova i messaggi che non sono mai partiti e vanno rispediti. La paginazione è a
cursore come nel resto dell'API e restituisce un total reale.
GET /v1/portability/messages/{id} restituisce il fascicolo probatorio di un messaggio: la riga CSV
esatta che è partita con il suo csvSha256, il destinatario e come è stato risolto,
l'identificativo SMTP del messaggio e annuaire — ciò che l'annuaire PPF rispondeva
per quel soggetto nel momento dell'invio. L'annuaire per noi è in sola lettura: un passaggio non vi si
scrive, si scrive nell'indirizzo di instradamento (passo 5) e da lì si propaga.
L'istantanea serve a provare il prima e verificare il dopo, confrontandola con quanto dice l'annuaire a
propagazione avvenuta — che è una chiamata a sé:
curl "…/v1/portability/annuaire/921376265" -H "Authorization: Bearer $FLOWIE_KEY"
GET /v1/portability/annuaire/{siren} risponde con la riga che decide dove vanno le fatture di quel
soggetto: currentPaMatricule (la piattaforma che lo instrada — 9998 è il
default PPF, quindi nessuno è stato dichiarato e potrebbe non esserci nulla da portare),
effectiveFrom, un effectiveTo quando una partenza è già programmata, e
isFlowie una volta che la commutazione si è propagata da noi. Da leggere prima di un passaggio
per sapere a chi lo si prende, e dopo per sapere se ha attecchito.
Il CSV a 18 campi
Una riga di intestazione e una riga di dati, separate da punto e virgola, in questo esatto ordine:
| # | Colonna | Nota |
|---|---|---|
| 1 | request_ref | Il vostro riferimento stabile per la richiesta. |
| 2 | message_type | REQUEST / ACK / DECISION / COMPLETION. |
| 3 | status_code | Il codice della tabella qui sopra. |
| 4 | direction_role | GAINING_PA o LOSING_PA. |
| 5 | taxpayer_siren | Nove cifre. |
| 6 | taxpayer_siret | Quattordici cifre. |
| 7 | taxpayer_name | Denominazione legale. |
| 8 | gaining_pa_id | Codice operatore o SIREN. |
| 9 | gaining_pa_name | |
| 10 | losing_pa_id | |
| 11 | losing_pa_name | |
| 12 | effective_date | Data di efficacia, ISO-8601. |
| 13 | transferred_addresses | Identificativi di instradamento, uniti da barre verticali. Inviate una lista, ricevete una lista. |
| 14 | mandate_ref | Il mandato di designazione. |
| 15 | mandate_signatory | Rappresentante legale. |
| 16 | request_datetime | ISO-8601 con fuso esplicito — la prova del termine. Predefinito: adesso (UTC). |
| 17 | reason_code | Atteso in caso di rifiuto. |
| 18 | reason_text |
Solo transferred_addresses si ripete, e usa | all'interno della cella per non entrare
mai in conflitto con il separatore. I valori mancanti sono scritti come stringhe vuote, mai omessi — è
il numero di colonne ciò che il parser valida.
Cosa resta provvisorio
Sviluppate contro di essi — servono a questo — ma trattate le stringhe esatte come soggette a
cambiamento, tenete il vostro requestRef come chiave di join e rileggete il
changelog prima di andare in produzione. Ciò che non cambierà è
la forma: un oggetto normalizzato, uno stato codificato, diciotto colonne e termini che partono nel momento in cui
una richiesta arriva.
Prossimi passi
- Importare una società — elenco completo di parametri e risposte.
- Endpoint di portabilità — costruzione e analisi, nella reference API.
- Conformità Francia — il quadro complessivo PPF e PA.
- Eventi — il registro da interrogare per seguire una migrazione.