Aller au contenu principal

Lots de paie — soumettre des missions pour calcul des bulletins

L'API partenaire Pesia vous permet de soumettre les missions d'une période (1 ligne = 1 mission) pour que Pesia calcule les bulletins de paie de vos intérimaires, puis de suivre la progression du calcul jusqu'à la clôture du lot.

Le lot de paie est canonique : il existe un seul lot par agence et par mois. Chaque POST alimente le lot de la période (créé automatiquement au premier dépôt) — il ne le remplace pas. Vous pouvez donc soumettre les missions au fil de l'eau, en plusieurs appels.

Migration du payload. Le groupe JSON end_user_company est remplacé par staffing_client_establishment et l'ancien nom n'est plus accepté. Pour les fichiers CSV, mission_end_user_siret devient mission_staffing_client_siret.

Prérequis​

  • Scope requis : payroll:simulate (voir Scopes) — il couvre la soumission et la lecture du statut des lots.
  • Le agencyCode doit correspondre à une agence de votre organisation. Ce code vous est retourné dans le champ code de GET /partner/v1/agencies avec le scope agency:read. Respectez sa casse exacte.
  • L'agence doit disposer d'une fiche légale, de paramètres de paie complets et, pour la period de chaque lot, d'un taux AT/MP intérimaire en vigueur le premier jour du mois. Ces données sont paramétrées côté Pesia : l'API ne permet pas de les renseigner. Si l'une manque, contactez votre interlocuteur Pesia (partenaires@pesia.fr).

Tous les montants sont en centimes (entiers) : 1250 = 12,50 €. Les heures sont des décimaux.

Soumettre des missions​

POST /partner/v1/payroll-batches accepte de 1 à 5 000 lignes par appel. Le header Idempotency-Key est obligatoire (UUIDv4 ou ULID).

Chaque ligne est complète et autonome : elle porte l'identité du salarié (groupe worker), les données de la mission (groupe mission) et les éléments variables éventuels (prime, indemnity) à la racine. Les lignes d'un même salarié (même worker.external_id, après retrait des blancs de début et de fin et normalisation Unicode NFC) sont regroupées sur le même bulletin.

Correspondance avec le format CSV. Si vous échangez aussi des fichiers CSV avec Pesia, la correspondance est mécanique : la colonne CSV worker_x = le champ JSON worker.x, worker_birth_x = worker.birth.x, worker_address_x = worker.address.x, mission_y = mission.y ; seule exception, mission_staffing_client_siret = staffing_client_establishment.siret. prime et indemnity sont identiques dans les deux formats. Deux tableaux de mission ne suivent pas cette règle : mission.remuneration_lines et mission.post_tax_deductions. Le CSV les porte dans des groupes de colonnes fixes. Voir Lignes de rémunération et Retenues après PAS.

Dans un fichier CSV, une colonne d'entiers (un montant en centimes, mission_days_worked) ne porte que des chiffres. Si une cellule contient 350.00, 350,00 ou 1e3, Pesia refuse tout le fichier, sans importer aucune ligne, et indique la ligne et la colonne en cause.

curl -X POST https://api.pesia.fr/partner/v1/payroll-batches \
-H "Authorization: Bearer <token>" \
-H "Idempotency-Key: 9b2f5c1e-7a3d-4e8b-a6c9-2d4f8e0b1a53" \
-H "X-Pesia-Env: production" \
-H "Content-Type: application/json" \
-d '{
"agencyCode": "AG-PARIS-01",
"period": "2026-07",
"paymentDate": "2026-08-05",
"rows": [
{
"worker": {
"external_id": "MAT-4207",
"last_name": "MARTIN",
"first_name": "Camille",
"nir": "2940369111222",
"sex": "02",
"birth": { "date": "1994-03-12", "city": "Lyon", "department": "69" },
"address": { "street": "12 rue de la République", "postal_code": "69002", "city": "Lyon" },
"iban": "FR1420041010050500013M02606",
"agency_entry_date": "2025-11-03"
},
"mission": {
"external_id": "MIS-2026-1001",
"contract_external_id": "CTT-2026-078451",
"start_date": "2026-07-15",
"end_date": "2026-07-24",
"hourly_rate": 1250,
"remuneration_lines": [
{ "type": "NORMAL", "hours": 45 },
{ "type": "OVERTIME", "hours": 4, "premiums": [{ "type": "OVERTIME", "rate": 0.25 }] }
],
"post_tax_deductions": [
{
"type": "EARNED_WAGE_INSTALLMENT",
"amount": 35000,
"event_date": "2026-07-20",
"external_reference": "ACP-42",
"label": "Acompte du 20 juillet"
}
],
"days_worked": 8,
"qualification": "Serveur",
"employee_category": "employe",
"pcs_ese_code": "561b"
},
"staffing_client_establishment": {
"siret": "13002526500013"
}
},
{
"worker": {
"external_id": "MAT-4207",
"last_name": "MARTIN",
"first_name": "Camille",
"nir": "2940369111222",
"sex": "02",
"birth": { "date": "1994-03-12", "city": "Lyon", "department": "69" },
"address": { "street": "12 rue de la République", "postal_code": "69002", "city": "Lyon" },
"iban": "FR1420041010050500013M02606",
"agency_entry_date": "2025-11-03"
},
"mission": {
"external_id": "MIS-2026-1002",
"contract_external_id": "CTT-2026-078460",
"start_date": "2026-07-27",
"end_date": "2026-07-31",
"hourly_rate": 1250,
"remuneration_lines": [{ "type": "NORMAL", "hours": 35 }],
"days_worked": 5,
"address": "4 avenue du 8 Mai 1945",
"postal_code": "69800",
"city": "Saint-Priest"
},
"staffing_client_establishment": {
"siret": "13002526500013"
}
},
{
"worker": {
"external_id": "MAT-9001",
"last_name": "BERNARD",
"first_name": "Alex",
"nir": "1880875123456",
"sex": "01",
"birth": { "date": "1988-08-15", "city": "Paris", "department": "75" },
"address": { "street": "8 avenue des Fleurs", "postal_code": "75012", "city": "Paris" },
"iban": "FR7630006000011234567890189",
"agency_entry_date": "2026-02-16"
},
"mission": {
"external_id": "MIS-2026-1015",
"contract_external_id": "CTT-2026-078502",
"start_date": "2026-07-06",
"end_date": "2026-07-10",
"hourly_rate": 1300,
"remuneration_lines": [{ "type": "NORMAL", "hours": 35 }],
"days_worked": 5,
"address": "10 rue de Charenton",
"postal_code": "75012",
"city": "Paris"
},
"staffing_client_establishment": {
"siret": "13002526500013"
},
"prime": 5000,
"indemnity": 1150
}
]
}'

Champs de la requête​

ChampTypeObligatoireDescription
agencyCodestringOuiValeur exacte du champ code retourné par GET /partner/v1/agencies avec le scope agency:read. Respectez la casse.
periodstringOuiMois de paie ciblé, format YYYY-MM, à partir de 1970-01.
paymentDatestringOuiJour de versement des salaires, format YYYY-MM-DD. Distinct de period : voir ci-dessous.
rowsobject[]OuiLignes du lot (1 ligne = 1 mission), 1 à 5 000 par appel.
period et paymentDate sont deux dates différentes

period est le mois travaillé. Il résout le SMIC, les minima de la convention collective et le taux AT/MP de l'agence : le taux intérimaire en vigueur le premier jour de ce mois.

paymentDate est le jour où les salaires sont versés. Il résout le barème du prélèvement à la source et son abattement pour contrat court. Pour un mois travaillé en juillet et payé le 5 août, envoyez "period": "2026-07" et "paymentDate": "2026-08-05".

Un lot porte une seule date de versement, reprise telle quelle sur la déclaration sociale nominative et sur l'ordre de virement. Envoyez la même paymentDate pour chaque soumission d'un lot. Pesia ne peut pas clôturer un lot dont les lignes portent des dates différentes : contactez votre interlocuteur Pesia.

Champs d'une ligne — obligatoires​

ChampTypeDescription
worker.external_idstringMatricule du salarié dans votre système (1 à 255 caractères). Clé de regroupement : toutes les lignes d'un même matricule alimentent le même bulletin. La DSN le déclare comme matricule s'il compte au plus 30 caractères, tous admis par le cahier technique DSN : lettres, chiffres, lettres accentuées usuelles, espace et ponctuation simple, sans ! # $ % ; < > ? [ \ ] ^ { | } ~. Une valeur faite uniquement de ponctuation, par exemple -, n'est pas déclarée. Une autre valeur, par exemple un UUID de 36 caractères, reste acceptée et regroupe bien les lignes, mais la DSN part sans matricule. À la réception, Pesia retire les blancs au début et à la fin du matricule, par exemple une espace, une espace insécable, une tabulation ou un retour à la ligne. Pesia le normalise ensuite en Unicode NFC : une lettre suivie d'un accent combinant devient la lettre accentuée, en un seul caractère. Pesia ne modifie aucun autre caractère. Ainsi MAT-1, et MAT-1 suivi d'une espace, désignent le même salarié. La limite de 255 caractères s'applique au matricule normalisé. Les réponses renvoient le matricule normalisé dans workerExternalId.
worker.last_namestringNom du salarié.
worker.first_namestringPrénom du salarié.
worker.nirstringNIR du salarié — 13 ou 15 caractères, avec ou sans espaces (2A/2B accepté en Corse).
worker.sexstring"01" homme, "02" femme.
worker.birth.datestringDate de naissance, format YYYY-MM-DD.
worker.address.streetstringAdresse du salarié — numéro et voie.
worker.address.postal_codestringCode postal (5 chiffres).
worker.address.citystringCommune.
worker.ibanstringIBAN du salarié — clé de contrôle vérifiée. Requis pour le virement du salaire et le bulletin.
worker.agency_entry_datestringDate d'entrée du salarié dans votre ETT, format YYYY-MM-DD — mention « Entrée dans l'ETT » du bulletin de paie.
mission.contract_external_idstringRéférence du contrat dans votre système. Utilisez la même valeur que le contract.external_id de vos DPAE pour relier contrat et bulletin. C'est cette référence qui nomme le contrat sur le bulletin détaillé (« Contrat CTR-1042 du … au … ») : sans elle, un bulletin multi-contrat n'est rapprochable d'aucun document.
mission.start_datestringPremier jour de la période payée par ce bulletin, format YYYY-MM-DD. Ce n'est pas le début du contrat : voir contract_start_date.
mission.end_datestringDernier jour de la période payée par ce bulletin, format YYYY-MM-DD (≥ début). Ce n'est pas le terme du contrat : voir contract_end_date.
mission.hourly_rateintegerTaux horaire en centimes, strictement positif (1250 = 12,50 €).
mission.remuneration_linesarrayHeures travaillées sur la mission, une entrée par combinaison de nature et de majorations — voir Lignes de rémunération. Au moins une heure est requise par ligne de lot, et le total d'une mission ne dépasse pas 744 h.
mission.days_workedintegerJours travaillés (1 à 31), borné à la durée calendaire de la mission.
staffing_client_establishment.siretstringSIRET valide de l'établissement où la mission est réalisée (14 chiffres et checksum Luhn).
Caractères acceptés par la DSN

La DSN n'accepte qu'une partie des caractères. Pesia convertit les textes à la déclaration, et le bulletin garde la valeur reçue :

  • nom et prénoms : lettres, lettres accentuées usuelles, espace, ' et -. « Ștefan » devient « Stefan », « N’Diaye » devient « N'Diaye » ;
  • commune du domicile : lettres sans accent, chiffres et espace. « Saint-Mandé » devient « Saint Mande » ;
  • voie et commune de naissance : les mêmes caractères que le nom, plus les chiffres et le point ;
  • mission.contract_external_id, déclaré comme numéro de contrat : lettres, chiffres, - . @ / _, espace et à é è ê ë ï ô ç. « CTR#2026 » devient « CTR 2026 ».

Un nom, un prénom, une voie ou une commune sans aucun caractère accepté, par exemple écrit dans un autre alphabet, bloque la clôture du lot. Corrigez les lignes du salarié et re-soumettez-les tant que le lot est ouvert.

Champs d'une ligne — optionnels​

ThèmeChampTypeDéfautDescription
Corrélationmission.external_idstring—Référence de la mission dans votre système — renvoyée telle quelle dans missions[] des réponses.
Identitéworker.nir_keystringcalculéeClé de contrôle du NIR, de 01 à 97. Elle doit correspondre au NIR.
Identitéworker.birth.citystring—Commune de naissance — libellé (ex. Lyon). Absent à l'import accepté ; la clôture du lot le refuse (DSN).
Identitéworker.birth.departmentstring—Département de naissance (01-95, 2A/2B, 971-976, 984-988, 99 à l'étranger). Absent à l'import accepté ; la clôture du lot le refuse (DSN).
Identitéworker.birth.countrystringFR si né en FrancePays de naissance — code ISO 3166-1 alpha-2. Requis à la clôture quand le département vaut 99.
Identitéworker.birth.insee_codestring—Code INSEE de la commune de naissance (complément optionnel du libellé).
Identitéworker.emailstring—Email du salarié.
Identitéworker.agency_exit_datestring—Date de sortie de l'ETT, format YYYY-MM-DD (≥ date d'entrée). Absente = salarié toujours inscrit ; l'absence remplace une sortie précédemment transmise (chaque lot transmet l'état complet).
Identitéworker.nationalitystringFRNationalité — code ISO 3166-1 alpha-2 (FR, PT, MA). Absent = FR pour la DSN, y compris pour un salarié né à l'étranger. Binational : indiquez FR si le salarié a la nationalité française. Apatride ou réfugié : non pris en charge dans cette version, contactez Pesia.
Protection socialeworker.reached_414hbooleanstatut en vigueur le mois de period, sinon falseSeuil des 414 heures atteint. Il fixe le taux de prévoyance et la répartition de la mutuelle intérimaires. Absent : Pesia reprend le statut du salarié en vigueur le mois de period, commun à toutes les agences de votre organisation, sinon false. Même valeur sur toutes les lignes du salarié, ou absent sur toutes : sinon le salarié est refusé (ok: false).
Protection socialeworker.health_insurance_enrolledbooleanadhésion en vigueur si le seuil ne change pas, sinon worker.reached_414hAdhésion à la mutuelle intérimaires, sous le seuil comme au-dessus. false au-dessus du seuil : dispense. Absent : Pesia reprend l'adhésion en vigueur le mois de period si le seuil ne change pas, sinon la valeur de worker.reached_414h. Salarié dispensé : envoyez false dès le lot où worker.reached_414h passe à true, sinon Pesia l'inscrit à la mutuelle. Même valeur sur toutes les lignes du salarié, ou absent sur toutes : sinon le salarié est refusé (ok: false).
Missionmission.qualificationstring—Qualification / poste, affiché sur le bulletin.
Missionmission.pcs_ese_codestring9999Code PCS-ESE de l'emploi (ex. 621a). Consultez la liste complète des codes et libellés acceptés, aussi lisible par GET /partner/v1/pcs-ese-codes. Absent, Pesia déclare 9999 (profession non connue) : la clôture n'est pas bloquée par ce champ, mais la DSN est moins précise.
Missionmission.employee_categorystringcatégorie par défaut de l'agenceCatégorie du salarié : cadre_dirigeant, cadre, agent_de_maitrise, employe, ouvrier. Pilote le statut cadre. Absent, Pesia applique la catégorie par défaut configurée pour l'agence ; si l'agence n'en a aucune, la clôture du lot refuse la ligne. Renseignez ce champ dès que la catégorie diffère du défaut de l'agence — un cadre déclaré sous un défaut ouvrier part en DSN sous le mauvais statut.
Missionmission.recourse_reasonstringaccroissement_activiteMotif de recours au travail temporaire : remplacement_salarie, accroissement_activite, emploi_saisonnier, contrat_usage, remplacement_chef_entreprise, remplacement_chef_exploitation_agricole, insertion_difficultes_sociales, complement_formation, apprentissage, remplacement_temps_partiel, attente_suppression_poste, interimaire_boeth.
Lieu de missionmission.addressstringétablissement utilisateurVoie du lieu de mission. Si ce champ est absent, Pesia utilise l'adresse de staffing_client_establishment. S'il est présent, postal_code et city sont obligatoires.
Lieu de missionmission.postal_codestringétablissement utilisateurCode postal du lieu de mission (5 chiffres), requis uniquement avec mission.address.
Lieu de missionmission.citystringétablissement utilisateurCommune du lieu de mission, requise uniquement avec mission.address.
Lieu de missionmission.insee_codestringcalculéCode INSEE de la commune du lieu de mission. Pesia le complète à partir du code postal et de la commune lorsqu'il est absent, ou le vérifie lorsqu'il est fourni.
Missionmission.ifm_exclusionstring—Cas d'exclusion légale de l'IFM pour la mission (art. L1251-33) — voir IFM et ICCP. Absent = IFM due (cas nominal).
Missionmission.post_tax_deductionsarray[]Retenues appliquées après le prélèvement à la source. Une mission accepte 20 entrées au maximum, et un même type peut revenir. Voir Retenues après PAS.
Contratcontract_start_datestringmission.start_datePremier jour du contrat, format YYYY-MM-DD. Obligatoire avec contract_end_date. Absent, le contrat est la mission de la ligne.
Contratcontract_end_datestringmission.end_dateFin du terme du contrat, format YYYY-MM-DD. Obligatoire avec contract_start_date. Terme précis : la fin inscrite au contrat. Terme imprécis : la fin de la durée minimale.
Contratcontract_term_kindstringterme précisprecis (date de fin nommée au contrat) ou imprecis (durée minimale seule). Dit ce que contract_end_date signifie. Absent, Pesia lit un terme précis.
Primesprimeinteger—Complément de rémunération du mois soumis à cotisations, en centimes.
Primesindemnityinteger—Indemnité non soumise (panier, transport…), en centimes, ajoutée au net.
Primesprime_labelstringPrimeLibellé de la prime manuelle sur le bulletin, de 1 à 200 caractères.
Primesindemnity_labelstringIndemnitéLibellé de l'indemnité manuelle sur le bulletin, de 1 à 200 caractères.
Mission longue payée par tranches : déclarez le contrat

Une ligne porte deux faits distincts, et ils ne se déduisent pas l'un de l'autre.

mission.start_date et mission.end_date délimitent la période payée par ce bulletin. Elles proratisent le plafond mensuel de sécurité sociale, et elles bougent à chaque bulletin d'une même mission.

contract_start_date et contract_end_date délimitent le contrat. Elles décident de l'abattement de prélèvement à la source réservé aux contrats de deux mois ou moins, et elles gardent la même valeur sur tous les bulletins d'un même contrat.

Tant qu'une ligne couvre une mission entière, les deux coïncident et vous n'avez rien à envoyer. Dès qu'une mission de plus de deux mois est payée en plusieurs tranches mensuelles, chaque tranche ressemble à un contrat court : renseignez contract_start_date et contract_end_date, sinon l'abattement s'applique à tort et le prélèvement est sous-évalué.

Pesia ne peut pas détecter l'oubli. Une dernière tranche porte exactement les mêmes dates qu'une mission courte entière.

Le champ prime couvre uniquement le complément du mois. Une prime non mensuelle requiert une qualification spécifique. Son libellé ne définit jamais cette qualification.

Les champs déclaratifs portent des valeurs métier. Pesia les traduit vers les codes de la norme DSN ; ne transmettez jamais un code DSN à leur place.

Retenues après PAS​

mission.post_tax_deductions porte les sommes à retirer après le prélèvement à la source. Le tableau accepte 20 entrées au maximum par mission. Cette limite borne la taille de la requête et ne porte aucune règle métier. Plusieurs entrées peuvent porter le même type : donnez alors à chacune sa propre external_reference pour les distinguer.

Le format CSV garde un seul groupe de colonnes par type, donc une seule entrée par type. Pour plusieurs entrées du même type, utilisez l'API.

ChampTypeDescription
typestringEARNED_WAGE_INSTALLMENT, SALARY_ADVANCE_REPAYMENT ou WAGE_GARNISHMENT.
amountintegerMontant en centimes. Utilisez une valeur de 1 à 100 000 000.
event_datestringDate facultative au format YYYY-MM-DD.
external_referencestringRéférence facultative. Le bulletin détaillé l'imprime. Utilisez 255 caractères au maximum, sans caractère de contrôle ni caractère invisible. Recommandée pour distinguer deux entrées du même type.
labelstringLibellé facultatif. Le bulletin détaillé l'imprime, sauf s'il répète le libellé du type. Le bulletin simplifié imprime seulement le total de chaque type. Utilisez 200 caractères au maximum, sans caractère de contrôle ni caractère invisible.

Le fichier CSV porte une retenue dans 4 colonnes, une par champ JSON du tableau ci-dessus. Une mission ne déclare qu'une entrée par type dans le fichier : pour plusieurs entrées du même type, utilisez l'API.

RetenueColonneChamp JSON
EARNED_WAGE_INSTALLMENTmission_earned_wage_installment_amountamount
EARNED_WAGE_INSTALLMENTmission_earned_wage_installment_event_dateevent_date
EARNED_WAGE_INSTALLMENTmission_earned_wage_installment_external_referenceexternal_reference
EARNED_WAGE_INSTALLMENTmission_earned_wage_installment_labellabel
SALARY_ADVANCE_REPAYMENTmission_salary_advance_repayment_amountamount
SALARY_ADVANCE_REPAYMENTmission_salary_advance_repayment_event_dateevent_date
SALARY_ADVANCE_REPAYMENTmission_salary_advance_repayment_external_referenceexternal_reference
SALARY_ADVANCE_REPAYMENTmission_salary_advance_repayment_labellabel
WAGE_GARNISHMENTmission_wage_garnishment_amountamount
WAGE_GARNISHMENTmission_wage_garnishment_event_dateevent_date
WAGE_GARNISHMENTmission_wage_garnishment_external_referenceexternal_reference
WAGE_GARNISHMENTmission_wage_garnishment_labellabel

Chaque colonne porte la même règle que son champ JSON, décrite dans le tableau ci-dessus. Le fichier CSV ajoute 4 règles propres à son format :

  • La colonne amount porte un entier en centimes, écrit avec des chiffres seuls : 35000 pour 350,00 €. Si la cellule contient 350.00, 350,00 ou 1e3, Pesia refuse tout le fichier, sans importer aucune ligne, et indique la ligne et la colonne en cause.
  • Une mission sans retenue d'un type laisse les 4 colonnes de ce type vides.
  • Une colonne event_date, external_reference ou label remplie exige la colonne amount du même type.
  • Ces 12 colonnes sont facultatives dans l'en-tête du fichier, et leur ordre n'est pas obligatoire.

Pesia additionne les entrées d'un même type sur toutes les missions du salarié. Pesia refuse le calcul si le total des retenues dépasse le net après PAS. Un total égal au net après PAS produit un net à verser nul et aucun virement salarié.

Ce refus n'apparaît pas dans la réponse du POST, qui reste 202. Le bulletin du salarié prend ensuite le statut FAILED avec l'erreur PARTNER_PAYROLL_PAYSLIP_COMPUTATION_FAILED (retryable: false — voir Suivre le lot), et failedCount augmente d'une unité. Corrigez les retenues, puis soumettez de nouveau les lignes du salarié : son bulletin repasse en PENDING, puis Pesia le calcule de nouveau.

Vous restez responsable de la légalité de chaque acompte pour un salarié intérimaire. Vous restez responsable du plafond de remboursement d'une avance. Vous calculez aussi la quotité saisissable. Pesia reçoit le montant final. Pesia ne calcule pas ces trois règles dans cette version.

Lignes de rémunération​

mission.remuneration_lines porte les heures de la mission. Une mission en déclare au plus six, et chacune sépare deux choses :

  • la nature des heures — NORMAL, OVERTIME (supplémentaires) ou COMPLEMENTARY (complémentaires, temps partiel) ;
  • les majorations qui s'y appliquent, chacune avec son taux explicite.

Une majoration de 0.25 augmente le taux horaire de 25 %. Ce n'est pas un coefficient : 1.25 serait une augmentation de 125 %. Les majorations d'une même ligne s'additionnent, et chacune s'applique au taux horaire de base.

ChampTypeDéfautDescription
typestring—NORMAL, OVERTIME ou COMPLEMENTARY. Requis.
hoursnumber—Heures de cette ligne (décimal, strictement positif). Requis.
premiumsarray[]Majorations appliquées à ces heures.
premiums[].typestring—OVERTIME, COMPLEMENTARY, NIGHT, SUNDAY ou HOLIDAY.
premiums[].ratenumber—Taux d'augmentation, strictement positif (0.25 = +25 %).
hourly_wage_centsintegermission.hourly_rateTaux horaire de base de cette ligne, en centimes. À renseigner uniquement si une ligne se paie à un autre taux que la mission.
overtime_kindstringVARIABLERéservé aux lignes OVERTIME : VARIABLE (aléatoires) ou STRUCTURAL (structurelles, inscrites au contrat). Le taux de majoration ne la détermine pas — des heures à +25 % peuvent relever de l'une ou l'autre nature, et la DSN les déclare séparément.

Deux règles de forme, et toutes deux sont des règles de droit :

  • une ligne OVERTIME porte obligatoirement une majoration OVERTIME, et une ligne COMPLEMENTARY une majoration COMPLEMENTARY — le code du travail place ce taux au-dessus de zéro, donc aucune valeur par défaut n'est possible ;
  • une majoration OVERTIME ou COMPLEMENTARY ne s'applique qu'à des heures de sa propre nature. NIGHT, SUNDAY et HOLIDAY s'appliquent à toutes les natures.

Un type de majoration n'apparaît qu'une fois par ligne : deux majorations de nuit sur les mêmes heures paieraient la nuit deux fois.

Exemple — des heures supplémentaires travaillées le dimanche​

Un salarié fait 70 heures normales, 13 heures supplémentaires à +25 %, 2 heures à +50 %, puis 8 heures supplémentaires un dimanche (+50 % de majoration dominicale) et 6 heures supplémentaires de nuit (+15 %). Taux horaire : 13,50 €.

"remuneration_lines": [
{ "type": "NORMAL", "hours": 70 },
{ "type": "OVERTIME", "hours": 13, "premiums": [{ "type": "OVERTIME", "rate": 0.25 }] },
{ "type": "OVERTIME", "hours": 2, "premiums": [{ "type": "OVERTIME", "rate": 0.5 }] },
{ "type": "OVERTIME", "hours": 8,
"premiums": [{ "type": "OVERTIME", "rate": 0.25 }, { "type": "SUNDAY", "rate": 0.5 }] },
{ "type": "OVERTIME", "hours": 6,
"premiums": [{ "type": "OVERTIME", "rate": 0.25 }, { "type": "NIGHT", "rate": 0.15 }] }
]

Le bulletin porte alors 99 heures et 1 507,28 € de brut au titre des heures :

RubriqueHeuresMontant
Heures normales70945,00 €
Heures supplémentaires (125 %)13219,38 €
Heures supplémentaires (150 %)240,50 €
Heures supplémentaires (125 %)8135,00 €
Majoration dimanche—54,00 €
Heures supplémentaires (125 %)6101,25 €
Majoration nuit—12,15 €

Les 8 heures du dimanche et les 6 heures de nuit restent trois lignes distinctes de Heures supplémentaires (125 %). Les fusionner étalerait la majoration dominicale sur 27 heures au lieu des 8 qui l'ont gagnée.

Les majorations du moment portent un montant sans heures : le décompte des heures du bulletin reste juste, et le salarié retrouve le nombre d'heures qu'il a travaillées.

Colonnes du fichier CSV​

Le fichier CSV porte chaque ligne de rémunération dans les colonnes du tableau ci-dessous. N est le numéro de la ligne de rémunération, de 1 à 6.

ColonneChamp JSON
mission_lineN_typetype
mission_lineN_hourshours
mission_lineN_premiumpremiums[].rate de la majoration OVERTIME ou COMPLEMENTARY, selon la nature de la ligne
mission_lineN_premium_nightpremiums[].rate de la majoration NIGHT
mission_lineN_premium_sundaypremiums[].rate de la majoration SUNDAY
mission_lineN_premium_holidaypremiums[].rate de la majoration HOLIDAY

Chaque colonne porte la même règle que son champ JSON, décrite dans le tableau des champs, plus haut dans cette section. Le fichier CSV ajoute 5 règles propres à son format :

  • L'en-tête nomme le type de chaque majoration : la cellule ne porte que son taux (0.25 = +25 %). La colonne mission_lineN_premium porte la majoration propre à la nature de la ligne. Remplissez-la sur une ligne OVERTIME ou COMPLEMENTARY, et laissez-la vide sur une ligne NORMAL. Sinon, Pesia refuse le fichier.
  • Numérotez les lignes de rémunération sans trou, à partir de N = 1 : si la ligne 3 est remplie, les lignes 1 et 2 le sont aussi. Un groupe de colonnes vide entre deux groupes remplis est refusé, et l'erreur nomme la colonne mission_lineN_type du groupe vide.
  • Écrivez les heures et les taux avec un point décimal : 7.5, 0.25. Une virgule est refusée.
  • Les colonnes d'une ligne de rémunération sont facultatives dans l'en-tête, et leur ordre n'est pas obligatoire. Une mission sans aucune heure est refusée.
  • Le fichier n'a pas de colonne pour hourly_wage_cents ni pour overtime_kind. Chaque ligne se paie au taux horaire de la mission (mission_hourly_rate), et ses heures supplémentaires sont VARIABLE. Pour un autre taux horaire, ou pour des heures STRUCTURAL, utilisez l'API.

Libellés des primes et indemnités​

Placez les libellés à la racine de chaque ligne, à côté des montants. Par exemple, ajoutez ces champs à une ligne de mission complète :

{
"prime": 2500,
"prime_label": "Majoration dimanche",
"indemnity": 1800,
"indemnity_label": "Frais kilométriques"
}

Les colonnes CSV portent les mêmes noms. Omettez un libellé dans le JSON, ou laissez sa cellule CSV vide, pour conserver « Prime » ou « Indemnité ». Un texte fourni contient de 1 à 200 caractères. Un libellé seul ne crée pas de ligne sur le bulletin : le montant associé doit être supérieur à zéro.

Le libellé change uniquement le texte du bulletin. Il ne change ni les cotisations, ni le code DSN. Il reste une seule prime et une seule indemnité manuelles par mission.

Indemnités sur le bulletin simplifié​

Pesia produit le bulletin sous deux présentations. Le bulletin détaillé présente chaque contrat séparément. Le bulletin simplifié regroupe les cotisations par catégorie, les retenues après PAS par type et les indemnités.

Le bloc « Indemnités et primes » contient votre champ indemnity et les autres indemnités du bulletin, par exemple le repas ou le blanchissage. Votre champ prime n'entre pas dans ce bloc : il figure dans les éléments de rémunération, avec une ligne par contrat.

Deux lignes se regroupent lorsque le libellé, le taux exact et l'unité sont identiques. La casse et les espaces du libellé comptent. Pour regrouper les indemnités de deux missions d'un même bulletin, envoyez le même indemnity_label, caractère pour caractère. Un taux différent reste sur une autre ligne, même si son arrondi affiché est identique.

La base est la somme des quantités pour une indemnité horaire ou journalière. Elle s'affiche alors sans unité, avec deux décimales au plus. Pour les autres lignes, la base est la somme des montants de base. Le montant est la somme des montants de chaque contrat. Il ne résulte pas d'un nouveau calcul sur la base totale.

Par exemple, quatre indemnités de blanchissage à 0,16 € de l'heure portent sur 3,83 h, 4,25 h, 4,08 h et 4,83 h. Leurs montants sont 0,61 €, 0,68 €, 0,65 € et 0,77 €. Quatre indemnités de repas portent chacune sur un jour à 2,30 €. Le bloc affiche alors :

DésignationBaseTauxMontant (€)
Indemnité de blanchissage16,990,16 €2,71 €
Indemnité de repas42,30 €9,20 €

La base du blanchissage compte des heures. La base du repas compte des jours. Un nouveau calcul sur la base totale donnerait 2,72 € (16,99 × 0,16 €). Le bulletin affiche 2,71 €, la somme des montants de chaque contrat.

Ce bloc ne porte aucun titre de contrat sur le bulletin simplifié. Les éléments de rémunération conservent leurs titres et leurs lignes par contrat. Le bulletin détaillé conserve sa présentation par contrat.

Adresse du lieu de mission​

Par défaut, omettez mission.address, mission.postal_code, mission.city et mission.insee_code : Pesia prend l'adresse et le code commune INSEE de staffing_client_establishment, enrichis via SIRENE. Ce cas correspondra dans l'application au choix « même adresse que l'entreprise utilisatrice ».

Si la mission se déroule ailleurs, fournissez ensemble mission.address, mission.postal_code et mission.city. Le champ mission.insee_code reste optionnel : Pesia le résout ou le vérifie via son référentiel de communes. Ce cas correspondra au choix « adresse différente de l'entreprise utilisatrice ».

Effectif de paie​

Pesia calcule chaque bulletin avec l'effectif de paie de l'organisation pour le mois du bulletin, en équivalents temps plein (ETP). Cet effectif détermine les taux liés à la tranche d'effectif, les contributions de mobilité et la DSN.

  • Si l'année précédente est définitive, l'effectif de paie est la borne basse de la tranche d'effectif de cette année : 0 (moins de 11 ETP), 11 (de 11 à 19), 20 (de 20 à 49), 50 (de 50 à 249) ou 250 (250 et plus). Une année est définitive quand elle est terminée et que son effectif est validé.
  • Sinon, ou si l'année précédente n'a aucun effectif, Pesia utilise l'effectif déclaré dans les paramètres de paie de l'agence.
  • Dans les deux cas, une exception PACTE (neutralisation du franchissement d'un seuil d'effectif) en cours au 1er du mois plafonne l'effectif de paie sous le seuil franchi. Par exemple, une exception sur le seuil de 20 ramène l'effectif à 11 au plus.

Pesia fige l'effectif de paie quand vous soumettez les lignes d'un salarié. Si Pesia vous signale un changement de l'effectif de paie pendant que le lot est ouvert, soumettez de nouveau les lignes de tous les salariés du lot, avec une nouvelle Idempotency-Key. Un lot dont les bulletins portent des effectifs différents ne peut pas être clôturé.

L'API ne renvoie pas l'effectif retenu. Pour le connaître, ou pour déclarer une exception PACTE, contactez votre interlocuteur Pesia (partenaires@pesia.fr).

Contributions de mobilité​

Quand l'effectif de paie atteint 11 ETP, VM et VMA suivent la commune du lieu de mission. VMRR suit la commune de l'entité légale de l'agence. Pesia calcule les contributions sur le bulletin. La DSN reprend ses assiettes, taux, montants et communes.

Une recherche réussie sans taux applicable signifie que la contribution ne s'applique pas. Une commune sans VMRR produit donc un montant VMRR nul. Une erreur technique fait échouer le calcul. Un lieu nécessaire absent bloque le calcul et la clôture définitive.

IFM et ICCP​

Les indemnités légales de fin de mission (IFM, 10 %) et compensatrice de congés payés (ICCP, 10 %, calculée sur le brut IFM comprise) sont calculées automatiquement par Pesia sur chaque bulletin intérim — vous n'avez rien à envoyer. Elles sont versées au fil de l'eau, chaque mois : sur une mission à cheval sur plusieurs mois, la somme des versements mensuels égale exactement 10 % de la rémunération totale de la mission — vous n'avez donc pas à renvoyer les mois précédents.

Si la mission relève d'un cas légal d'exclusion de l'IFM (art. L1251-33), renseignez mission.ifm_exclusion avec l'un des codes suivants — l'IFM est alors mise à zéro et la raison est tracée sur le bulletin :

CodeCas
CDI_PROPOSE_ACCEPTECDI proposé par l'entreprise utilisatrice à l'issue de la mission, accepté
CDI_PROPOSE_REFUSECDI équivalent proposé, refusé par le salarié
FAUTE_GRAVERupture anticipée pour faute grave du salarié
FORCE_MAJEURERupture pour force majeure
RUPTURE_ANTICIPEE_SALARIERupture anticipée à l'initiative du salarié
CONTRAT_SAISONNIERContrat saisonnier (si prévu par accord)
CONTRAT_USAGEContrat d'usage (si prévu par accord)
PERIODE_ESSAIRupture pendant la période d'essai

L'ICCP reste toujours due pour un contrat CTT — la loi ne prévoit aucun cas d'exclusion : c'est pourquoi seul l'IFM porte ce champ.

Le corps est validé dans son ensemble : une ligne invalide (champ manquant, format incorrect, dates incohérentes…) rejette toute la requête en 400, avec le détail par ligne. Une fois le corps accepté, le traitement devient fail-soft par salarié (voir ci-dessous).

Réponse 202 Accepted​

Les missions sont prises en charge et les salariés sont dispatchés vers le calcul asynchrone des bulletins. La réponse détaille le résultat salarié par salarié — le traitement est fail-soft : un salarié en échec ne bloque pas les autres.

{
"success": true,
"data": {
"mode": "production",
"lotId": "0d5c2a4e-9f1b-4c7d-8e3a-6b2f4a8c0d15",
"period": "2026-07",
"lotStatus": "OPEN",
"workers": [
{
"workerExternalId": "MAT-4207",
"ok": true,
"missions": [
{ "missionExternalId": "MIS-2026-1001", "contractExternalId": "CTT-2026-078451" },
{ "missionExternalId": "MIS-2026-1002", "contractExternalId": "CTT-2026-078460" }
]
},
{
"workerExternalId": "MAT-9001",
"ok": false,
"error": {
"code": "SIRENE_STAFFING_CLIENT_CLOSED",
"message": "L'établissement utilisateur est fermé pour la période de mission.",
"retryable": false
},
"missions": [
{ "missionExternalId": "MIS-2026-1015", "contractExternalId": "CTT-2026-078502" }
]
}
]
},
"timestamp": "2026-08-10T12:00:00.000Z"
}
  • mode — production ou sandbox (voir Sandbox).
  • lotId — identifiant du lot canonique alimenté. Conservez-le pour le suivi via GET /partner/v1/payroll-batches/{lotId}. Il peut être null si aucun salarié n'a pu être pris en compte (tous en ok: false), ou en mode sandbox.
  • lotStatus — statut du lot au moment de la réponse (voir cycle de vie du lot).
  • workers[] — un élément par salarié (regroupement par worker.external_id) :
    • ok: true — le salarié est pris en compte. Pesia lance le calcul de son bulletin ; un calcul refusé prend ensuite le statut FAILED ;
    • ok: false — le salarié n'a pas été pris en compte ; corrigez ses lignes et re-soumettez-les. Les autres salariés du lot ont été traités normalement ;
    • error.code — code stable exploitable par votre intégration ;
    • error.retryable — indique si une nouvelle tentative est recommandée. Pour un quota SIRENE, error.retryAfterMs précise le délai minimal ;
    • missions[] — vos références externes (mission.external_id, mission.contract_external_id), renvoyées pour corrélation.

Rejouer la même requête avec la même Idempotency-Key renvoie la même réponse (même lotId) sans retraiter les missions.

Un lot par agence et par mois​

Le lot de paie est l'unité de traitement mensuelle : Pesia calcule les bulletins des salariés du lot, puis clôture le lot au moment du traitement de la paie du mois.

StatutSignification
OPENLot ouvert — vos soumissions alimentent ce lot.
CLOSINGClôture engagée — les derniers bulletins se finalisent.
CLOSEDLot clôturé. Toute nouvelle soumission sur la période renvoie 409.

Pour toute question sur la clôture d'un lot (calendrier, correction après clôture), contactez votre interlocuteur Pesia (partenaires@pesia.fr).

Suivre le lot​

curl https://api.pesia.fr/partner/v1/payroll-batches/0d5c2a4e-9f1b-4c7d-8e3a-6b2f4a8c0d15 \
-H "Authorization: Bearer <token>" \
-H "X-Pesia-Env: production"

Réponse 200 OK :

{
"success": true,
"data": {
"lotId": "0d5c2a4e-9f1b-4c7d-8e3a-6b2f4a8c0d15",
"agencyCode": "AG-PARIS-01",
"period": "2026-07",
"lotStatus": "OPEN",
"expectedCount": 3,
"computedCount": 1,
"failedCount": 2,
"closedAt": null,
"workers": [
{
"workerExternalId": "MAT-4207",
"computed": true,
"status": "COMPUTED",
"missions": [
{ "missionExternalId": "MIS-2026-1001", "contractExternalId": "CTT-2026-078451", "pcsEseCode": "561b" },
{ "missionExternalId": "MIS-2026-1002", "contractExternalId": "CTT-2026-078460", "pcsEseCode": "9999" }
]
},
{
"workerExternalId": "MAT-9001",
"computed": false,
"status": "FAILED",
"error": {
"code": "PARTNER_PAYROLL_PAYSLIP_COMPUTATION_FAILED",
"message": "Le calcul du bulletin a échoué.",
"retryable": false
},
"missions": [
{ "missionExternalId": "MIS-2026-1015", "contractExternalId": "CTT-2026-078502", "pcsEseCode": "9999" }
]
},
{
"workerExternalId": "MAT-9002",
"computed": false,
"status": "FAILED",
"error": {
"code": "PARTNER_PAYROLL_PAYSLIP_COMPUTATION_TEMPORARY_FAILURE",
"message": "Le calcul du bulletin est temporairement indisponible. Soumettez de nouveau les lignes du salarié.",
"retryable": true
},
"missions": [
{ "missionExternalId": "MIS-2026-1016", "contractExternalId": "CTT-2026-078503", "pcsEseCode": "621a" }
]
}
]
},
"timestamp": "2026-08-10T12:00:00.000Z"
}
  • expectedCount — salariés attendus dans le lot.
  • computedCount — salariés dont le bulletin porte le statut COMPUTED.
  • failedCount — salariés dont le bulletin porte le statut FAILED (les deux causes ci-dessous confondues).
  • closedAt — horodatage de clôture (ISO 8601, UTC), null tant que le lot est ouvert.
  • workers[].status — statut du calcul : PENDING, COMPUTED ou FAILED.
  • workers[].computed — compatibilité avec les versions précédentes. La valeur est true uniquement pour le statut COMPUTED.
  • workers[].missions[].pcsEseCode — code PCS-ESE que Pesia déclare en DSN pour la mission. 9999 (profession non connue) indique que la ligne ne portait pas de mission.pcs_ese_code, ou portait 9999. Pour déclarer la profession réelle, renseignez le code de l'emploi. Le champ est absent quand Pesia ne détient aucun code valide pour la mission. La réponse du POST ne porte pas ce champ.
  • workers[].error — présent uniquement pour le statut FAILED. Il ne contient aucun diagnostic interne, et prend l'une de ces deux formes fixes :
    • code: "PARTNER_PAYROLL_PAYSLIP_COMPUTATION_FAILED", retryable: false — le calcul refuse les lignes soumises. Corrigez-les avant de les soumettre à nouveau ;
    • code: "PARTNER_PAYROLL_PAYSLIP_COMPUTATION_TEMPORARY_FAILURE", retryable: true — indisponibilité temporaire. Soumettez de nouveau les mêmes lignes du salarié, avec une nouvelle Idempotency-Key. Espacez vos nouvelles tentatives d'un délai croissant (par exemple 1 min, puis 5 min, puis 15 min) et arrêtez après 3 essais. Si l'échec persiste au-delà de ces essais, contactez votre interlocuteur Pesia (partenaires@pesia.fr) plutôt que de soumettre de nouveau les mêmes lignes en boucle.

Le calcul d'un lot se termine généralement en quelques minutes. Une boucle de polling raisonnable — toutes les 60 secondes, jusqu'à ce que chaque bulletin porte le statut COMPUTED ou FAILED :

while true; do
body=$(curl -s https://api.pesia.fr/partner/v1/payroll-batches/<lotId> \
-H "Authorization: Bearer <token>" \
-H "X-Pesia-Env: production")
echo "$body" | jq '{lotStatus: .data.lotStatus, computed: .data.computedCount, failed: .data.failedCount, expected: .data.expectedCount}'
[ "$(echo "$body" | jq '(.data.computedCount + .data.failedCount) == .data.expectedCount')" = "true" ] && break
sleep 60
done

Pour être notifié de la publication des bulletins sans interroger l'API en boucle, abonnez-vous à l'événement payroll.payslip.published du catalogue Webhooks.

Sandbox​

Avec un accès sandbox (header X-Pesia-Env: sandbox), le dépôt est un dry-run : le payload et le contexte agence (fiche légale, paramètres de paie, taux AT/MP intérimaire de la période) sont validés, mais rien n'est persisté et aucun bulletin n'est calculé. La réponse porte mode: "sandbox", lotId: null et lotStatus: null ; workers[].ok reflète la validation de chaque salarié et workers[].error décrit les lignes refusées.

Le sandbox permet de valider votre intégration — format des lignes, regroupement par salarié, corrélation par références externes, idempotence — sans polluer un vrai lot de paie (voir Sandbox).

Erreurs​

Toutes les erreurs suivent le format application/problem+json (RFC 7807), comme le reste de l'API.

Après acceptation du corps, une erreur limitée à un salarié reste dans la réponse 202 via workers[].error. Les codes SIRENE_STAFFING_CLIENT_* demandent une correction métier ; les codes SIRENE dont retryable vaut true signalent une indisponibilité temporaire.

Statut HTTPCauseAction recommandée
400Corps invalide (champ manquant ou mal formé, lot vide ou > 5 000 lignes, dates incohérentes, IBAN ou NIR invalide).Corrigez la requête à partir du détail par ligne fourni dans la réponse.
400Fiche légale ou paramètres de paie de l'agence incomplets.Contactez votre interlocuteur Pesia (partenaires@pesia.fr) pour compléter la configuration de l'agence.
400Contexte de paie de l'agence refusé (PARTNER_PAYROLL_BATCH_INVALID_INPUT) : aucun taux AT/MP intérimaire en vigueur le premier jour de period, ou donnée de la fiche légale absente.Vérifiez period. Si elle est correcte, contactez votre interlocuteur Pesia (partenaires@pesia.fr) en citant le code agence, la période et le detail. Voir PARTNER_PAYROLL_BATCH_INVALID_INPUT.
400idempotency_key_required / idempotency_key_invalid_format.Fournissez un header Idempotency-Key au format UUIDv4 ou ULID.
403insufficient_scope.Le scope payroll:simulate manque à votre grant — voir Scopes.
404Agence (agencyCode) ou lot (lotId) introuvable.Vérifiez le champ code retourné par GET /partner/v1/agencies, ou l'identifiant de lot retourné au dépôt.
409Le lot de paie de l'agence pour la période est clôturé — soumission impossible.Le mois est clos : aucune mission ne peut plus y être ajoutée. Contactez votre interlocuteur Pesia si une régularisation est nécessaire.
409idempotency_key_conflict — même clé réutilisée avec un corps différent.Générez une nouvelle Idempotency-Key par soumission distincte.

Voir aussi​

  • Scopes — scope payroll:simulate
  • Codes PCS-ESE — codes acceptés, aussi lisibles par GET /partner/v1/pcs-ese-codes
  • Idempotence — header Idempotency-Key
  • DPAE — déposer les DPAE des mêmes contrats de mission
  • Webhooks — événement payroll.payslip.published
  • Sandbox — environnement de test