Retour au guide

Paie, dépenses et comptabilité par l'API

Deux façons d'adresser une société, puis les employés, les fiches de salaire, les absences, les dépenses, les relevés bancaires et l'état financier annuel, en appels REST.

Développeurs8 min de lecture
Table des matières+

Ce guide couvre tout ce qui n'est pas de la facturation. Même clé, même base URL que dans Démarrer avec l'API :

BASE=https://<deployment>.convex.site/api/v1
KEY=ba_...

1. Deux façons d'adresser une société

Les routes de paie sont adressées par société comme toutes les autres ressources : /companies/{companyId}/employees et …/payslips. La même clé fait la paie de toutes les sociétés auxquelles vous avez accès — prenez le companyId dans GET /companies.

Tout le reste est adressé explicitement : absences, dépenses, transactions bancaires et états financiers vivent sous /companies/{companyId}/…, et une même clé y sert pour toutes les sociétés accessibles, à condition que le module correspondant soit activé.

COMPANY=<companyId>

2. Employés

curl -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/employees

La liste est paginée (?limit, ?cursor, ordre de création) et renvoie chaque employé avec son identité, son adresse, son contrat, son salaire et ses taux d'assurance. La création reprend le jeu de champs du formulaire de l'app :

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "firstName": "Anna", "lastName": "Muster",
    "address": { "kind": "swiss", "country": "CH", "street": "Rue du Lac", "streetNumber": "1",
                 "postalCode": "1700", "city": "Fribourg", "canton": "FR", "communeBfs": 2196 },
    "permitType": "swiss", "contractType": "cdi", "maritalStatus": "single",
    "grossMonthlySalary": 6500, "workPercentage": 100,
    "startDate": "2026-01-01", "vacationWeeks": 5,
    "thirteenthSalaryEnabled": true,
    "language": "de"
  }' $BASE/companies/$COMPANY/employees

La réponse 201 contient id et slug. Le champ facultatif language (fr, de, en, it) est la langue de la fiche de salaire : libellés stockés, PDF et e-mail d'envoi ; omis, la fiche suit la langue du propriétaire du compte. Une modification partielle recalcule les fiches non envoyées de l'employé, comme une édition à l'écran :

curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "grossMonthlySalary": 6800 }' $BASE/companies/$COMPANY/employees/<employeeId>

À prévoir : 403 EMPLOYEE_LIMIT_REACHED quand le quota d'employés du plan est atteint, 422 END_DATE_REQUIRED_FOR_CDD pour un contrat à durée déterminée sans date de fin, 422 END_DATE_BEFORE_START_DATE si elle précède le début.

3. Fiches de salaire

curl -H "Authorization: Bearer $KEY" "$BASE/companies/$COMPANY/payslips?year=2026&month=7"
curl -H "Authorization: Bearer $KEY" "$BASE/companies/$COMPANY/payslips?employeeId=<employeeId>"

Liste paginée, du plus récent au plus ancien. year et month vont toujours ensemble (l'un sans l'autre est un 400 VALIDATION_ERROR) ; employeeId filtre par personne — une page filtrée peut être courte, paginez jusqu'à ce que nextCursor soit null. La création calcule la fiche avec toutes les vérifications de l'app :

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "employeeId": "<employeeId>", "year": 2026, "month": 7, "canton": "FR" }' \
  $BASE/companies/$COMPANY/payslips

Facultatif : variableItems (bonus, heures supplémentaires, lignes libres) et overrides. Réponses à prévoir : 409 PAYSLIP_ALREADY_EXISTS si la période existe déjà pour cet employé, 422 PAYSLIP_PERIOD_OUTSIDE_EMPLOYMENT hors de la fenêtre d'emploi, 422 MISSING_IS_BAREME quand aucun barème d'impôt à la source n'est chargé pour ce canton et cette année, 403 PAYSLIP_LIMIT_REACHED au bout du quota mensuel.

Pour un mois entier, le lot accepte de 1 à 100 fiches et n'est pas atomique : chaque élément est traité seul, et la réponse donne le résultat de chacun. Une erreur en cours de lot ne défait pas les fiches déjà créées.

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "payslips": [
        { "employeeId": "<id1>", "year": 2026, "month": 7, "canton": "FR" },
        { "employeeId": "<id2>", "year": 2026, "month": 7, "canton": "FR" }
      ] }' $BASE/companies/$COMPANY/payslips/batch
{
  "data": {
    "results": [
      { "index": 0, "status": "created", "id": "..." },
      { "index": 1, "status": "error", "code": "PAYSLIP_ALREADY_EXISTS" }
    ]
  }
}

Le PDF se télécharge comme celui d'une facture, avec -L pour suivre la redirection 302 vers une URL valable 15 minutes ; le premier téléchargement d'une fiche peut prendre quelques secondes, le temps de la générer.

curl -L -H "Authorization: Bearer $KEY" -o payslip.pdf $BASE/companies/$COMPANY/payslips/<payslipId>/pdf

4. Absences

Les absences sont adressées par société (/companies/{companyId}/absences, module paie), à la différence des employés. Déclarer une maladie à 50 % sur deux semaines :

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "employeeId": "<employeeId>", "motif": "maladie",
        "startDate": "2026-03-02", "endDate": "2026-03-13",
        "incapacityPercent": 50 }' \
  $BASE/companies/$COMPANY/absences

employeeId, motif et startDate sont obligatoires. motif vaut maladie, accident, apg_maternite, apg_paternite, apg_adoption, apg_prise_en_charge, apg_service, maintien_salaire, conge_court ou conge_non_paye. Les dates sont ici des chaînes YYYY-MM-DD, pas des millisecondes, parce que c'est ainsi que les absences sont stockées et triées ; une absence sans endDate est en cours.

  • dailyAmountOverride : indemnité journalière communiquée par l'assureur, qui court-circuite le taux et le plafond légal.
  • waitingDaysOverride : jours de carence de la police maladie.
  • ratePercentOverride : taux de 0 à 100, par défaut 80 ou celui configuré pour l'employé.
  • takenAsIsolatedDays : paternité ou adoption prise en jours isolés (complément APG 7 pour 5).
  • incapacityPercent : 1 à 100, par défaut 100 ; et notes.

La réponse 201 renvoie l'absence, toujours en statut declared. 409 ABSENCE_OVERLAPS_EXISTING signale qu'une autre absence du même employé couvre déjà certains de ces jours : un chevauchement doublerait les lignes de retenue et d'indemnité, découpez les périodes.

curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "motif": "maladie", "startDate": "2026-03-02", "endDate": "2026-03-20" }' \
  $BASE/companies/$COMPANY/absences/<absenceId>

Le statut a sa propre sous-route : declaredapprovedpaid, un simple marqueur de suivi, n'importe quelle cible est acceptée depuis n'importe quel état.

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "status": "approved" }' \
  $BASE/companies/$COMPANY/absences/<absenceId>/status

Il n'y a volontairement pas de filtre par période : une absence en cours n'a pas de fin, et un filtre par mois la laisserait tomber. Parcourez l'année et filtrez côté client, ou restreignez par employeeId. Supprimer une absence ne retire pas les lignes déjà comptabilisées sur une fiche de salaire ; seul un recalcul de la fiche le fait.

5. Dépenses

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "vendorName": "Swisscom", "status": "toPay", "documentDate": "2026-02-28",
        "currency": "CHF", "total": 129.9, "category": "telecom" }' \
  $BASE/companies/$COMPANY/expenses

vendorName, status, documentDate, currency, total et category sont obligatoires ; la ventilation TVA (subtotal, vatRate, vatAmount) et les identifiants du fournisseur sont facultatifs. status vaut toPay ou paid. category est l'une des 16 catégories fixes : rent, insurance, goodsPurchases, subcontracting, officeSupplies, itSoftware, telecom, vehicle, travel, meals, marketing, professionalFees, utilities, bankFees, training, other.

year est dérivé de documentDate, et paidDate suit status : posé à maintenant quand la dépense est créée payée sans date, effacé dès que le statut n'est plus paid. Lister une année, puis marquer une dépense payée :

curl -H "Authorization: Bearer $KEY" "$BASE/companies/$COMPANY/expenses?year=2026"

curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "status": "paid" }' \
  $BASE/companies/$COMPANY/expenses/<expenseId>

Le justificatif stocké est exposé en lecture seule, avec la même redirection 302 que les PDF ; une dépense créée sans justificatif (hasDocument: false) répond 404 :

curl -L -H "Authorization: Bearer $KEY" -o receipt.pdf \
  $BASE/companies/$COMPANY/expenses/<expenseId>/document

Hors périmètre en v1 : l'envoi d'un justificatif (pas d'endpoint multipart), l'extraction par IA, et les brouillons d'import Drive en attente de validation, invisibles ici. Codes à prévoir : 403 SUPPLIER_BILL_LIMIT_REACHED au bout du quota mensuel de dépenses, 403 CURRENCY_PRO_ONLY pour une devise autre que CHF sur un plan Free.

6. Transactions bancaires

Il n'y a pas de POST unitaire : l'app ne crée des transactions que par lots, un fichier importé égale un lot, et le lot est le point d'entrée.

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "fileName": "ubs-2026-01.csv",
        "transactions": [
          { "date": "2026-01-15", "amount": -1250, "currency": "CHF",
            "label": "Loyer janvier", "bankAccount": "CH93 0076 2011 6238 5295 7",
            "category": "rent" }
        ] }' \
  $BASE/companies/$COMPANY/bank-transactions/batch

transactions contient de 1 à 100 lignes (400 BATCH_SIZE_INVALID sinon), chacune avec au moins date, amount, currency, label et bankAccount. amount est signé : un encaissement est positif, un débit négatif. Le lot est une seule transaction de base de données, tout est écrit ou rien ne l'est. importBatchId regroupe les lignes d'un même fichier ; omis, il est généré et la réponse vous le donne. Une category envoyée est enregistrée comme choix manuel que l'heuristique d'import n'écrasera jamais.

{ "data": { "inserted": 1, "importBatchId": "9f1c…" } }
curl -H "Authorization: Bearer $KEY" "$BASE/companies/$COMPANY/bank-transactions?year=2026"

curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "category": "insurance", "color": "#3c8fc3" }' \
  $BASE/companies/$COMPANY/bank-transactions/<transactionId>

year est l'année civile de la date de valeur ; un exercice qui ne clôt pas en décembre en chevauche deux, laissez le filtre de côté pour parcourir tout l'historique. PATCH accepte category (marquée manuelle) et color, une étiquette #rrggbb ou "" pour l'effacer ; DELETE répond 204.

7. État financier

L'état financier annuel est une ressource par société et par exercice : fiscalYear est l'année de clôture (l'exercice 2025/26 d'une société qui clôt en juin est 2026) et sert d'identifiant, d'où une paire GET/PUT sur l'année plutôt qu'une collection.

curl -H "Authorization: Bearer $KEY" \
  $BASE/companies/$COMPANY/financial-statements/2026

La lecture renvoie l'input stocké, le cliché ratesUsed et, une fois que le générateur de l'app l'a produit, le document calculé sous snapshot. 404 si rien n'est stocké pour cette année. L'écriture est un upsert sur (société, exercice) :

curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "input": { "accounts": [ { "id": "CH93…", "currency": "CHF", "openingBalance": 12000 } ],
                   "manual": { "tradeReceivables": 0, "otherShortTermReceivables": 0,
                               "fixedAssets": 0, "ownerCurrentAccountBalance": 0,
                               "tradePayables": 0, "otherShortTermDebts": 0,
                               "avsBalance": 0, "lppBalance": 0, "accruedLiabilities": 0,
                               "taxProvision": 0, "shareCapital": 20000, "legalReserve": 0,
                               "retainedEarnings": 0, "depreciation": 0,
                               "proposedDividend": 0, "proposedReserveAllocation": 0,
                               "employeeCount": 2 },
                   "closingRates": { "EUR": 0.93 } } }' \
  $BASE/companies/$COMPANY/financial-statements/2026

Le corps porte l'entrée du formulaire de génération : les comptes bancaires confirmés avec leur solde d'ouverture, les postes saisis à la main (manual), les taux de change de clôture et un period facultatif. Le snapshot est une donnée dérivée, produite par le générateur, jamais dans la requête : un PUT qui l'omet laisse le document stocké intact. period n'est nécessaire que pour un exercice qui n'est pas l'année civile, et son to doit tomber dans fiscalYear (422 PERIOD_FISCAL_YEAR_MISMATCH sinon).

8. Mandats fiduciaires

Pour une fiduciaire, /mandates est une route de clé, sans plan ni module : elle liste les sociétés clientes que vous possédez. mandateId est simplement le companyId du mandat. Créer un mandat crée une société :

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "name": "Muster AG" }' $BASE/mandates

Dès lors, chaque route /companies/{companyId}/… s'applique au mandat avec ce même identifiant — employés, fiches de salaire et absences comprises : la même clé liste les employés d'un mandat et en fait la paie.

Pour les règles valables partout (pagination, dates, corps stricts, codes d'erreur, sandbox), lisez Conventions et erreurs ; le parcours de facturation est dans Facturer par l'API. Le détail de chaque champ est dans la Swagger UI de votre déploiement (…/api/docs).