Portabilité (changement de PA)
Un assujetti peut changer de Plateforme Agréée à tout moment, et il conserve son adressage fondé sur le SIREN/SIRET en le faisant — l'annuaire du PPF garantit la portabilité de l'identifiant, donc rien en aval n'a à être réadressé. Ce qui doit avoir lieu, en revanche, c'est une reprise entre les deux plateformes : la PA entrante demande, la PA sortante répond, et l'annuaire bascule à une date d'effet convenue.
Ce guide couvre les deux sens, parce que Flowie joue les deux rôles : entrant (un assujetti a choisi Flowie, nous émettons la demande) et sortant (une autre PA nous reprend un assujetti, nous devons répondre dans le délai légal). Quatre endpoints couvrent l'échange — deux pour embarquer la société, deux pour parler le format de fil inter-PA.
Les délais à tenir
La portabilité est un problème d'échéances avant d'être un problème d'intégration. Trois règles commandent tout ce qui suit :
- Accuser réception sous 24 heures après réception de la demande.
- Décider sous 5 jours ouvrés : les week-ends et les jours fériés ne comptent pas.
- Le silence vaut accord : passé le délai, la reprise se fait sans l'approbation de la plateforme sortante.
Un accusé de réception manqué est un manquement en soi, indépendamment de la
décision que vous auriez prise. Horodatez chaque message émis et reçu — la colonne
request_datetime du CSV ci-dessous existe exactement pour cette preuve.
1 · Importer l'assujetti depuis son SIRET
Le parcours portail ne vous donne qu'une entrée : le SIRET de l'assujetti. Tout le
reste en découle. Flowie prend le SIREN sur les neuf premiers chiffres, en déduit le pays
FR, construit l'identifiant Peppol 0009:<siren>, et résout la raison
sociale — ainsi que la PA actuelle — depuis l'annuaire du PPF.
curl -X POST …/v1/companies/import \
-H "Authorization: Bearer $FLOWIE_KEY" \
-H "Content-Type: application/json" \
-d '{
"siret": "92137626500017",
"mode": "portability"
}'
Trois issues, par ordre de priorité :
- Import — vous avez passé un
sovosCompanyId: la société existante est récupérée et son identifiant fiscal, son nom et ses capacités font foi. - Provisionnement — pas d'identifiant de société mais une organisation connue (champ de la requête ou valeur par défaut configurée) : une connexion gérée est provisionnée depuis le SIRET.
- Local seul — ni l'un ni l'autre : la société est enregistrée en
pending_verificationet un événementcompany.import.pendingest écrit pour que les ops fassent le lien plus tard.
L'embarquement n'échoue jamais durement faute d'identifiant backend, ce qui compte quand une demande de reprise arrive avant que le contractuel soit bouclé. L'appel est idempotent sur le SIRET : le rejouer resynchronise l'enregistrement existant au lieu d'en créer un second, donc un retry après timeout est sûr.
Vous récupérez l'organisation avec son id et son peppolId. Ne
renseignez companyName ou countryCode que pour surcharger ce que résout
l'annuaire — omettez-les et l'annuaire l'emporte.
2 · Importer en masse
Une migration de plateforme déplace des centaines de sociétés d'un coup.
POST /v1/companies/import/batch prend une liste des mêmes objets et les traite
concurremment, cinq à la fois.
curl -X POST …/v1/companies/import/batch \
-H "Authorization: Bearer $FLOWIE_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{"siret": "92137626500017"},
{"siret": "55208131766522"}
]
}'
La réponse contient une ligne de résultat par élément, dans l'ordre d'entrée :
{
"results": [
{"index": 0, "status": "imported", "companyId": "org_…", "peppolId": "0009:921376265"},
{"index": 1, "status": "failed", "error": "siret must be 14 digits"}
]
}
Un élément en échec ne coule pas le lot. L'appel renvoie quand même
200 avec un résultat partiel : contrôlez chaque ligne plutôt que le code de statut
— index renvoie à la position dans votre requête. Comme chaque élément
emprunte le même chemin idempotent, renvoyer le lot entier pour rejouer les échecs ne dupliquera pas
ceux qui étaient déjà passés.
3 · Envoyer le message inter-PA
Le canal que l'AIFE impose entre plateformes est l'e-mail, avec un objet normalisé, un
statut codifié et un CSV à 18 champs. POST /v1/portability/messages assemble ce message,
l'envoie par e-mail à la plateforme d'en face et le consigne — vous n'avez jamais
à formater un objet à la main, ni à chercher où l'envoyer, ni à perdre la preuve
que vous l'avez envoyé.
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"
}'
Vous récupérez l'identifiant de journal, l'objet, le CSV sous forme d'en-tête plus ligne et son empreinte, à qui le message a été adressé et comment cette adresse a été trouvée, et s'il est bien parti :
{
"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 quand vous êtes la plateforme entrante,
gainingPaName quand vous êtes la sortante — et elle est résolue depuis
l'annuaire des Plateformes Agréées immatriculées : la boîte
dédiée à la portabilité que la plateforme a publiée si elle en a une, son
courriel de contact DGFiP sinon. recipientSource vous dit ce qui s'est passé
(registry:<nom>, explicit si vous avez passé to vous-même,
ou unresolved si le nom ne correspond à rien). to l'emporte toujours.
dispatched vaut
false et reason dit pourquoi — dispatch_disabled,
not_configured (pas de relais ou pas d'expéditeur), no_recipient (aucune adresse
à qui l'adresser), sandbox, ou l'erreur SMTP elle-même. La ligne du journal est
écrite dans tous les cas : un environnement à sec produit la même piste d'audit sans l'e-mail,
et une panne de relais vous laisse le message exact à ré-envoyer plutôt qu'un trou. Un environnement
hors production peut aussi forcer un destinataire de repli : le message résout et consigne la vraie contrepartie,
mais il est remis à l'adresse de repli — tester n'écrit jamais à une vraie plateforme.
PORTABILITY_CHANNEL_FROM, que le corps du message indique. Ce
circuit joint les fichiers par référence : le CSV voyage donc en ligne dans le corps
— identique octet pour octet au csvRow retourné et à ce qui a été
haché. S'il vous faut le CSV en pièce jointe .csv, forcez le relais direct
(PORTABILITY_TRANSPORT=smtp). transport, dans la réponse et dans le journal, dit
lequel a porté le message.
Les quatre valeurs de messageType correspondent aux étapes de l'échange —
REQUEST, ACK, DECISION, COMPLETION — tandis que
state est votre état interne, traduit pour vous en code de statut de fil (voir
États de la demande). directionRole dit qui parle :
GAINING_PA ou LOSING_PA.
La grammaire de l'objet est stricte et positionnelle :
[PORTABILITE][<MESSAGE_TYPE>][<STATUS_CODE>][SIREN:<9 chiffres>][REF:<request_ref>]
Un SIREN qui ne fait pas exactement neuf chiffres, un type de message inconnu, ou un requestRef
contenant ] est rejeté en 400 avant toute construction.
4 · Analyser un message entrant
L'autre moitié : retransformer un message reçu en champs structurés. Passez l'objet, et la ligne CSV quand vous l'avez.
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 réponse vous donne le type de message, le code de statut de fil, l'state interne
correspondant, le SIREN, la référence de la demande, et — si une ligne a été
fournie — les 18 colonnes analysées dans fields.
Un objet qui ne respecte pas la grammaire renvoie 400 : mettez-le en rebut, n'ouvrez pas de
demande à partir de lui. C'est tout l'intérêt d'un objet normalisé — ce
qui ne s'analyse pas n'est pas un message de portabilité, et deviner son intention est le meilleur moyen de
faire migrer le mauvais assujetti. Une ligne CSV qui n'a pas exactement 18 colonnes est rejetée de la
même façon.
5 · Basculer le routage à la date d'effet
S'accorder sur une reprise ne change rien en soi. Ce qui décide où part une facture, c'est
l'adresse de facturation électronique de l'assujetti côté conformité — c'est
elle que le répertoire finit par router — et POST /v1/portability/routing est ce qui la
déplace :
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 }
}
Tout est dans la date. En tant que plateforme entrante, vous déclarez l'adresse avec
validFrom = la date d'effet : une reprise conclue en août pour le 1er octobre ne
capte pas les factures dès août. En tant que plateforme sortante
(role: "LOSING_PA"), rien n'est supprimé : l'émission s'arrête à la date
d'effet tandis que la réception reste ouverte jusqu'à date d'effet + 12 mois — le
service minimal que la LFI 2026 impose à la plateforme sortante, pour que les flux en cours aboutissent.
Ajustez la fenêtre avec minimalServiceMonths si un contrat promet plus long.
L'appel est idempotent sur le SIREN : une adresse déjà déclarée pour lui est mise
à jour, jamais dupliquée. Si l'organisation a plusieurs connexions, vous devez désigner la
bonne avec connectionId — en choisir une à votre place, c'est ainsi qu'une reprise
atterrit sur la mauvaise société. Et si vous embarquez l'assujetti dans le même mouvement,
POST /v1/companies/import accepte désormais la même effectiveDate et la
reporte sur l'adresse qu'il crée.
L'annuaire des PA
Une demande de reprise ne vaut que par ce que vous savez de l'endroit où l'autre plateforme lit son
courrier. GET /v1/portability/platforms est cet annuaire — tous les opérateurs
immatriculés par la DGFiP, avec l'adresse à laquelle un message de portabilité doit
réellement partir :
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"
}
Il fusionne les deux listes officielles de la DGFiP — les opérateurs remplissant toutes les
conditions (status: "registered") et ceux encore en attente des tests d'interopérabilité
("pending_interop") — avec les boîtes de portabilité dédiées que les
plateformes se sont communiquées entre elles. contactEmail est celle qui compte : l'adresse
dédiée quand elle existe, le contact DGFiP générique sinon. Filtrez avec q
(nom, e-mail ou site) et status.
La liste bouge chaque semaine au fil des immatriculations : la réponse porte donc sa propre
snapshotDate et la source dont elle est issue. S'il vous faut la liste faisant foi
à l'instant t, c'est cette source.
États de la demande
Une demande de portabilité traverse huit états. Chacun correspond à un code court porté par l'objet et par le CSV, pour qu'une contrepartie puisse router automatiquement dessus :
| État | Code de fil | Signification |
|---|---|---|
received | REQ | Demande créée ou message entrant analysé. Démarre les compteurs 24 h et 5 jours. |
acknowledged | ACK | Réception confirmée sous 24 h. Le premier délai est tenu. |
accepted | ACC | Décision favorable, dans la fenêtre de 5 jours. |
rejected | REJ | Refus. Un motif est attendu — renseignez reasonCode et reasonText. |
auto_accepted | TAC | Accord tacite : le délai est passé sans décision, donc le silence vaut accord. |
executing | MIG | Date d'effet atteinte ; la bascule de l'annuaire est en cours. |
completed | CMP | L'annuaire confirme que la nouvelle plateforme est active. |
failed | ERR | Erreur de transport ou d'annuaire. |
Envoyez l'état interne dans state ; vous n'écrivez jamais le code de fil vous-même.
Au retour, parse résout le code vers l'état pour vous. Un état inconnu donne un
400, pas un passage silencieux.
Les deux états de décision diffèrent par qui les déclenche. accepted et
rejected sont un acte délibéré dans la fenêtre ; auto_accepted
est ce qui arrive à la partie silencieuse quand la fenêtre se referme. Si vous êtes la
plateforme sortante, un TAC sur votre demande est le signal que vous avez manqué
l'échéance.
Suivre une demande
L'embarquement de société écrit dans le journal d'événements, et c'est là qu'on suit une migration aujourd'hui :
curl "…/v1/events?type=company.imported&limit=100" \
-H "Authorization: Bearer $FLOWIE_KEY"
Deux types d'événements sont écrits par le chemin d'import : company.imported
quand l'enregistrement est actif, et company.import.pending quand la société est
restée locale et attend un rattachement par les ops. Chacun porte l'identifiant de la
société, l'identifiant Peppol, le SIRET, le SIREN et le pays, ce qui suffit à
réconcilier une migration de masse ligne à ligne. La liste est paginée par curseur —
continuez à passer le cursor retourné jusqu'à ce que hasMore soit
false.
GET /v1/events mais ne figurent
pas au catalogue des webhooks : abonner un webhook à company.imported ne délivrera rien.
Interrogez le journal pour l'instant ; la référence
webhooks liste ce qui est réellement poussé.
Chaque message envoyé est consigné, et le journal s'interroge :
curl "…/v1/portability/messages?requestRef=POR-2026-000123" \
-H "Authorization: Bearer $FLOWIE_KEY"
GET /v1/portability/messages les liste du plus récent au plus ancien, limités à
votre organisation, filtrables par requestRef, siren, state,
messageType et dispatched — une référence rejoue donc tout un
échange, et dispatched=false retrouve les messages qui ne sont jamais partis et qu'il faut
ré-envoyer. La pagination est par curseur comme ailleurs dans l'API, avec un total réel.
GET /v1/portability/messages/{id} renvoie le dossier de preuve d'un message : la ligne CSV exacte
qui est partie avec son csvSha256, le destinataire et la façon dont il a été
résolu, l'identifiant SMTP du message, et annuaire — ce que l'annuaire PPF répondait
pour cet assujetti au moment de l'envoi. L'annuaire nous est en lecture seule : une reprise ne s'y écrit
pas, elle s'écrit dans l'adresse de routage (étape 5) et s'y propage.
L'instantané sert à prouver l'avant et vérifier l'après, en le comparant à ce
que dit l'annuaire une fois la propagation faite — ce qui fait l'objet d'un appel dédié :
curl "…/v1/portability/annuaire/921376265" -H "Authorization: Bearer $FLOWIE_KEY"
GET /v1/portability/annuaire/{siren} renvoie la ligne qui décide où partent les
factures de cet assujetti : currentPaMatricule (la plateforme qui le route — 9998
est le défaut PPF, donc personne n'est déclaré et il n'y a peut-être rien à
reprendre), effectiveFrom, un effectiveTo quand un départ est déjà
programmé, et isFlowie une fois la bascule propagée chez nous. À lire avant une
reprise pour savoir à qui on la prend, et après pour savoir si elle a pris.
Le CSV à 18 champs
Une ligne d'en-tête et une ligne de données, séparées par des points-virgules, dans cet ordre exact :
| # | Colonne | Notes |
|---|---|---|
| 1 | request_ref | Votre référence stable pour la demande. |
| 2 | message_type | REQUEST / ACK / DECISION / COMPLETION. |
| 3 | status_code | Le code de fil du tableau ci-dessus. |
| 4 | direction_role | GAINING_PA ou LOSING_PA. |
| 5 | taxpayer_siren | Neuf chiffres. |
| 6 | taxpayer_siret | Quatorze chiffres. |
| 7 | taxpayer_name | Raison sociale. |
| 8 | gaining_pa_id | Code opérateur ou SIREN. |
| 9 | gaining_pa_name | |
| 10 | losing_pa_id | |
| 11 | losing_pa_name | |
| 12 | effective_date | Date d'effet, ISO-8601. |
| 13 | transferred_addresses | Identifiants de routage, joints par des barres verticales. Envoyez une liste, récupérez une liste. |
| 14 | mandate_ref | Le mandat de désignation. |
| 15 | mandate_signatory | Représentant légal. |
| 16 | request_datetime | ISO-8601 avec fuseau explicite — la preuve du délai. Vaut maintenant (UTC) par défaut. |
| 17 | reason_code | Attendu sur un refus. |
| 18 | reason_text |
Seule transferred_addresses se répète, et elle utilise | à
l'intérieur de la cellule pour ne jamais entrer en collision avec le séparateur. Les valeurs
manquantes sont écrites comme chaînes vides, jamais omises — c'est le nombre de colonnes que
l'analyseur valide.
Ce qui reste provisoire
Développez contre eux — c'est à cela qu'ils servent — mais traitez les chaînes
exactes comme susceptibles de changer, gardez votre propre requestRef comme clé de jointure, et
relisez le changelog avant la mise en production. Ce qui ne changera pas, c'est la
forme : un objet normalisé, un statut codifié, dix-huit colonnes, et des délais qui partent
dès qu'une demande arrive.
La suite
- Importer une société — liste complète des paramètres et des réponses.
- Endpoints de portabilité — construction et analyse, dans la référence API.
- Conformité France — le tableau d'ensemble PPF et PA.
- Événements — le journal à interroger pour suivre une migration.