Flowie
Guide

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.

Fuori dalla Francia?
Non c'è nessuna Plateforme Agréée da lasciare, né un passaggio di consegne regolamentato. Cosa comporta un cambio in tutti gli altri paesi europei — la modifica nel registro, l'autorizzazione da riassegnare, chi conserva l'archivio — è trattato in Cambiare piattaforma in Europa, che offre anche un modulo di richiesta di migrazione senza alcuna chiave API.

I termini da rispettare

La portabilità è un problema di scadenze prima ancora che di integrazione. Tre regole governano tutto quello che segue:

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à:

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"
}
Non serve conoscere l'indirizzo e-mail dell'altra piattaforma
Nominate la controparte — 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.
Il messaggio viene registrato che parta o no
L'invio è subordinato a un interruttore generale e alla configurazione SMTP, e una chiave sandbox non raggiunge mai una piattaforma reale. Quando la mail non parte, 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.
Da dove esce
Il messaggio esce attraverso il circuito di posta di Flowie: porta quindi l'identità di mittente e la recapitabilità della piattaforma, non un relay noto a questo solo servizio; la controparte risponde all'indirizzo di 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:

StatoCodiceSignificato
receivedREQRichiesta creata o messaggio in entrata analizzato. Avvia i termini di 24 h e 5 giorni.
acknowledgedACKRicezione confermata entro 24 h. Il primo termine è rispettato.
acceptedACCDecisione favorevole, entro la finestra di 5 giorni.
rejectedREJRifiuto. È atteso un motivo — valorizzate reasonCode e reasonText.
auto_acceptedTACAssenso tacito: il termine è scaduto senza decisione, quindi il silenzio vale assenso.
executingMIGData di efficacia raggiunta; la commutazione dell'annuario è in corso.
completedCMPL'annuario conferma che la nuova piattaforma è attiva.
failedERRErrore 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.

Sono voci del registro eventi, non eventi webhook
Si leggono tramite 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.

Non c'è ancora un timer lato server
Nulla invia un riscontro al vostro posto e nulla porta una richiesta a silence vaut accord allo scadere del quinto giorno lavorativo. Il registro dei messaggi vi dà le marche temporali che provano i termini; i termini restano a carico vostro.

Il CSV a 18 campi

Una riga di intestazione e una riga di dati, separate da punto e virgola, in questo esatto ordine:

#ColonnaNota
1request_refIl vostro riferimento stabile per la richiesta.
2message_typeREQUEST / ACK / DECISION / COMPLETION.
3status_codeIl codice della tabella qui sopra.
4direction_roleGAINING_PA o LOSING_PA.
5taxpayer_sirenNove cifre.
6taxpayer_siretQuattordici cifre.
7taxpayer_nameDenominazione legale.
8gaining_pa_idCodice operatore o SIREN.
9gaining_pa_name
10losing_pa_id
11losing_pa_name
12effective_dateData di efficacia, ISO-8601.
13transferred_addressesIdentificativi di instradamento, uniti da barre verticali. Inviate una lista, ricevete una lista.
14mandate_refIl mandato di designazione.
15mandate_signatoryRappresentante legale.
16request_datetimeISO-8601 con fuso esplicito — la prova del termine. Predefinito: adesso (UTC).
17reason_codeAtteso in caso di rifiuto.
18reason_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

Il formato di trasmissione è un modello di lavoro, non un formato certificato
La grammatica dell'oggetto, i codici di stato e l'insieme delle colonne del CSV sono la lettura che Flowie dà del processo, in attesa della pubblicazione dell'allegato AIFE del 15/07, non ancora indicizzato pubblicamente. Sono centralizzati in un unico modulo, proprio perché la riconciliazione sia una singola modifica ben testata anziché una migrazione sparsa.

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