Retour au guide

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

Les employés, la création des fiches de salaire, les absences, les dépenses, les relevés bancaires et les comptes annuels, en appels REST.

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

Ce guide couvre ce qui n'est pas de la facturation, sauf le déroulé mensuel des fiches de salaire, détaillé dans Cycle de paie par l'API. Même clé, même base URL que dans Démarrer avec l'API :

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

1. 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 de l'employé encore en brouillon, du mois en cours et des suivants, 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.

DELETE …/employees/{employeeId} répond 204 et supprime aussi ses fiches de salaire, ses absences et ses liens d'espace employé. Il est refusé (409 PAYSLIP_DECLARED) si l'une de ses fiches est déclarée.

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. hoursWorked est obligatoire pour un employé payé à l'heure. 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

Les aperçus, la validation, l'envoi, la correction, la duplication et les PDF par lot sont détaillés dans Cycle de paie par l'API.

4. Absences

Les absences sont adressées par société (/companies/{companyId}/absences, module paie). 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 '{ "kind": "supplierInvoice", "vendorName": "Swisscom", "status": "toPay", "documentDate": "2026-02-28",
        "currency": "CHF", "total": 129.9, "accountId": "<accountId>" }' \
  $BASE/companies/$COMPANY/expenses

kind, vendorName, status, documentDate, currency et total sont obligatoires ; la ventilation TVA (subtotal, vatRate, vatAmount) et les identifiants du fournisseur sont facultatifs. status vaut toPay ou paid. kind vaut supplierInvoice, expenseReport, vatReturn, taxAssessment, socialStatement, shareholderResolution, financingContract ou other. taxAssessment et socialStatement exigent un documentNumber qui indique la nature ou le régime et la période.

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. La liste renvoie les factures fournisseurs par défaut, ?kind= choisit un autre type. 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 se télécharge avec la même redirection 302 que les PDF ; une dépense sans justificatif (hasDocument: false) répond 404 :

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

Pour l'attacher, envoyez le fichier lui-même en corps de requête, sans multipart (PDF, PNG, JPEG ou WebP, 10 Mo au maximum). L'opération ne se fait qu'une fois : une dépense qui a déjà un justificatif répond 409 DOCUMENT_ALREADY_ATTACHED.

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/pdf" \
  --data-binary @receipt.pdf $BASE/companies/$COMPANY/expenses/<expenseId>/document

POST …/expenses/bulk-status et POST …/expenses/bulk-delete traitent de 1 à 100 identifiants et répondent affected et skipped : un identifiant inconnu est compté dans skipped, sans bloquer les autres. Si votre abonnement ne couvre pas le module Comptabilité, prévoyez aussi 403 SUPPLIER_BILL_LIMIT_REACHED au bout du quota mensuel de dépenses et 403 CURRENCY_PRO_ONLY pour une devise autre que CHF.

Brouillons de dépense

Un brouillon est une dépense en attente de revue : il n'apparaît pas dans GET …/expenses et ne consomme le quota de dépenses qu'une fois accepté. Une société garde au plus 100 brouillons en attente, import Google Drive compris (409 EXPENSE_DRAFT_LIMIT_REACHED). Deux façons d'en créer :

# Lecture du document par IA, en un appel
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/pdf" \
  --data-binary @receipt.pdf "$BASE/companies/$COMPANY/expense-drafts/extract?locale=fr&filename=receipt.pdf"

# Sans IA : téléverser le fichier, puis créer le brouillon avec vos propres valeurs
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/pdf" \
  --data-binary @receipt.pdf $BASE/companies/$COMPANY/uploads/expense

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "kind": "supplierInvoice", "vendorName": "Swisscom", "status": "toPay", "documentDate": "2026-02-28",
        "currency": "CHF", "total": 129.9, "uploadId": "<uploadId>" }' \
  $BASE/companies/$COMPANY/expense-drafts

…/extract répond 201 avec draft, readabilityScore et quota. Chaque lecture réussie consomme une unité du quota mensuel d'extraction (200 en Business), un échec de lecture ne le consomme pas. Codes à prévoir : 403 EXPENSE_AI_QUOTA_REACHED, 422 SCAN_UNREADABLE (envoyez un meilleur scan), 429 AI_RATE_LIMITED et 502 AI_REQUEST_FAILED (réessayez).

…/uploads/expense renvoie un uploadId valable une heure. POST …/expense-drafts le consomme, sans IA ni quota d'extraction, et DELETE …/uploads/expense/{uploadId} abandonne un fichier non utilisé. Un identifiant expiré répond 403 UPLOAD_EXPIRED, un identifiant créé par un autre utilisateur ou pour une autre société 403 FILE_ACCESS_DENIED.

Pour la revue : GET …/expense-drafts (paginé), GET …/expense-drafts/{draftId}, GET …/expense-drafts/{draftId}/document pour le fichier et PATCH …/expense-drafts/{draftId} pour corriger un champ. Puis acceptez ou rejetez :

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "ids": ["<draftId>"] }' $BASE/companies/$COMPANY/expense-drafts/accept

…/accept et …/reject prennent de 1 à 100 ids et répondent affected et skipped. Un identifiant inconnu est compté dans skipped, mais une facture fournisseur sans compte fait échouer tout l'appel avec 422 CLASSIFICATION_ACCOUNT_REQUIRED : corrigez-la d'abord avec PATCH. Le rejet est définitif, le fichier part avec le brouillon.

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",
            "bankReference": "CH9300762011623852957:2026-0001" }
        ] }' \
  $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. Un accountId envoyé classe la ligne, comme un choix manuel que l'heuristique d'import n'écrasera jamais.

{ "data": { "inserted": 1, "skipped": 0, "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 '{ "comment": "Loyer à vérifier", "color": "#3c8fc3" }' \
  $BASE/companies/$COMPANY/bank-transactions/<transactionId>

year est l'année civile de la date de comptabilisation (date) ; 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 les champs de la ligne, color, une étiquette #rrggbb ou "" pour l'effacer, et accountId, null pour retirer le classement ; DELETE répond 204.

Pour plusieurs lignes : POST …/bank-transactions/bulk-delete, …/bulk-color et …/bulk-account prennent de 1 à 100 ids et répondent affected et skipped ; …/bulk-accounts prend des paires { id, accountId }. …/matching/delete et …/matching/account agissent sur ce qu'un filtre sélectionne dans une année (year obligatoire), par tranches : répétez le même corps tant que done vaut false. …/categorize fait classer de 1 à 100 lignes par IA et consomme le quota mensuel de préremplissage (100 en Business).

7. Comptes annuels

Les comptes annuels sont une ressource en lecture seule 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). L'API sert la version que la société a arrêtée dans l'app, une fois l'exercice clôturé dans le grand livre.

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

La lecture renvoie la version en vigueur telle que son PDF l'imprime : les lignes balanceSheet et incomeStatement avec leurs libellés figés et les montants de l'exercice et de l'exercice précédent, la proposition d'emploi du bénéfice appropriation (SA, Sàrl) ou la répartition entre associés partnerAllocation (sociétés de personnes), ainsi que le numéro de version, la language du document et le pdfStatus. 404 tant que les comptes ne sont pas arrêtés, puis à nouveau après une réouverture de l'exercice.

8. Mandats fiduciaires

Pour une fiduciaire, /mandates est une route de clé, sans plan ni module. GET /mandates liste vos mandats sans pagination : les sociétés que vous possédez (kind: owned, id égal au companyId), puis vos demandes et accès sur des sociétés gérées par un autre compte (kind: external, id de la demande, companyId de la société). ?includeClosed=true ajoute les mandats archivés et les demandes closes. 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

La réponse 201 renvoie le mandat. Le corps prend name (avec ide en option), ide seul si la société est déjà sur Bill Alps, ou companyId seul. Une société qui appartient déjà à un autre compte n'est pas dupliquée : la réponse est 202, avec une demande pending, et ses administrateurs reçoivent un e-mail. À prévoir : 402 SUBSCRIPTION_REQUIRED sans abonnement fiduciaire actif, 403 FIDUCIAIRE_ONLY sur un compte qui n'est pas fiduciaire, 409 IDE_ALREADY_USED et 422 INVALID_IDE sur le numéro IDE.

Quatre actions sans corps complètent la liste, sur /mandates/{mandateId}/… : archive et restore pour un mandat que vous possédez, cancel pour retirer une demande en attente, leave pour rendre un accès accepté.

Dès lors, chaque route /companies/{companyId}/… s'applique au mandat avec son companyId, employés, fiches de salaire et absences comprises, tant que le plan du mandat est business : 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 mois de paie dans Cycle de paie par l'API. Le détail de chaque champ est dans la Swagger UI de votre déploiement (…/api/docs).