Zurück zur Anleitung

Lohn, Ausgaben und Buchhaltung per API

Zwei Arten, eine Firma anzusprechen, dann Mitarbeitende, Lohnabrechnungen, Abwesenheiten, Ausgaben, Kontoauszüge und der Jahresabschluss, in REST-Aufrufen.

Entwickler8 Min. Lesezeit
Inhaltsverzeichnis+

Diese Anleitung deckt alles ab, was nicht Fakturierung ist. Derselbe Schlüssel, dieselbe Basis-URL wie in Erste Schritte mit der API:

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

1. Zwei Arten, eine Firma anzusprechen

Die Lohnrouten sind wie alle anderen Ressourcen über die Firma adressiert: /companies/{companyId}/employees und …/payslips. Derselbe Schlüssel macht den Lohn aller Firmen, auf die Sie Zugriff haben — die companyId liefert GET /companies.

Alles andere wird explizit adressiert: Abwesenheiten, Ausgaben, Banktransaktionen und Jahresabschlüsse liegen unter /companies/{companyId}/…, und ein Schlüssel dient dort für alle zugänglichen Firmen, sofern das passende Modul aktiv ist.

COMPANY=<companyId>

2. Mitarbeitende

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

Die Liste ist paginiert (?limit, ?cursor, Reihenfolge der Erstellung) und liefert jede Person mit Identität, Adresse, Vertrag, Lohn und Versicherungssätzen. Das Erstellen übernimmt den Feldsatz des App-Formulars:

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

Die Antwort 201 enthält id und slug. Das optionale language (fr, de, en, it) ist die Sprache der Lohnabrechnung: gespeicherte Bezeichnungen, PDF und Versand-E-Mail; fehlt es, folgt die Abrechnung der Sprache des Kontoinhabers. Eine Teiländerung berechnet die nicht versandten Abrechnungen der Person neu, wie eine Bearbeitung am Bildschirm:

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

Zu erwarten: 403 EMPLOYEE_LIMIT_REACHED, wenn das Mitarbeiterkontingent des Plans erreicht ist, 422 END_DATE_REQUIRED_FOR_CDD für einen befristeten Vertrag ohne Enddatum, 422 END_DATE_BEFORE_START_DATE, wenn es vor dem Beginn liegt.

3. Lohnabrechnungen

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

Paginierte Liste, Neueste zuerst. year und month gehören immer zusammen (eines ohne das andere ist ein 400 VALIDATION_ERROR); employeeId filtert nach Person — eine gefilterte Seite kann kurz sein, blättern Sie, bis nextCursor null ist. Das Erstellen berechnet die Abrechnung mit allen Prüfungen der 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

Optional: variableItems (Bonus, Überstunden, freie Positionen) und overrides. Zu erwartende Antworten: 409 PAYSLIP_ALREADY_EXISTS, wenn die Periode für diese Person schon existiert, 422 PAYSLIP_PERIOD_OUTSIDE_EMPLOYMENT ausserhalb des Anstellungsfensters, 422 MISSING_IS_BAREME, wenn für Kanton und Jahr kein Quellensteuertarif geladen ist, 403 PAYSLIP_LIMIT_REACHED nach Ausschöpfen des Monatskontingents.

Für einen ganzen Monat nimmt der Stapel 1 bis 100 Abrechnungen und ist nicht atomar: Jedes Element wird für sich verarbeitet, und die Antwort meldet das Ergebnis jedes einzelnen. Ein Fehler mitten im Stapel macht bereits erstellte Abrechnungen nicht rückgängig.

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" }
    ]
  }
}

Das PDF lädt man wie das einer Rechnung, mit -L, um der 302-Weiterleitung auf eine 15 Minuten gültige URL zu folgen; der erste Download einer Abrechnung kann ein paar Sekunden dauern, solange sie erzeugt wird.

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

4. Abwesenheiten

Abwesenheiten sind firmenbezogen (/companies/{companyId}/absences, Lohnmodul), anders als die Mitarbeitenden. Eine Krankheit zu 50 % über zwei Wochen erfassen:

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 und startDate sind Pflicht. motif ist maladie, accident, apg_maternite, apg_paternite, apg_adoption, apg_prise_en_charge, apg_service, maintien_salaire, conge_court oder conge_non_paye. Daten sind hier YYYY-MM-DD-Strings, keine Millisekunden, weil Abwesenheiten so gespeichert und sortiert werden; eine Abwesenheit ohne endDate läuft noch.

  • dailyAmountOverride: das vom Versicherer mitgeteilte Taggeld, das Satz und gesetzliche Obergrenze umgeht.
  • waitingDaysOverride: Wartetage der Krankentaggeldpolice.
  • ratePercentOverride: Satz von 0 bis 100, standardmässig 80 oder der für die Person konfigurierte Satz.
  • takenAsIsolatedDays: Vaterschaft oder Adoption in einzelnen Tagen bezogen (EO-Aufstockung 7 für 5).
  • incapacityPercent: 1 bis 100, standardmässig 100; und notes.

Die Antwort 201 liefert die Abwesenheit, immer im Status declared. 409 ABSENCE_OVERLAPS_EXISTING meldet, dass eine andere Abwesenheit derselben Person einige dieser Tage schon abdeckt: Eine Überschneidung würde die Abzugs- und Taggeldzeilen verdoppeln, teilen Sie die Zeiträume auf.

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>

Der Status hat seine eigene Unterroute: declaredapprovedpaid, eine blosse Markierung, jedes Ziel wird aus jedem Zustand akzeptiert.

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

Es gibt bewusst keinen Periodenfilter: Eine laufende Abwesenheit hat kein Ende, und ein Monatsfilter würde sie fallen lassen. Blättern Sie durch das Jahr und filtern Sie clientseitig, oder grenzen Sie mit employeeId ein. Das Löschen einer Abwesenheit entfernt bereits auf einer Lohnabrechnung verbuchte Zeilen nicht; das tut nur eine Neuberechnung der Abrechnung.

5. Ausgaben

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 und category sind Pflicht; die MwSt.-Aufschlüsselung (subtotal, vatRate, vatAmount) und die Lieferantenkennungen sind optional. status ist toPay oder paid. category ist eine von 16 festen Kategorien: rent, insurance, goodsPurchases, subcontracting, officeSupplies, itSoftware, telecom, vehicle, travel, meals, marketing, professionalFees, utilities, bankFees, training, other.

year wird aus documentDate abgeleitet, und paidDate folgt status: auf jetzt gesetzt, wenn die Ausgabe ohne Datum als bezahlt erstellt wird, gelöscht, sobald der Status nicht mehr paid ist. Ein Jahr auflisten, dann eine Ausgabe als bezahlt markieren:

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>

Der gespeicherte Beleg ist schreibgeschützt zugänglich, mit derselben 302-Weiterleitung wie die PDFs; eine ohne Beleg erstellte Ausgabe (hasDocument: false) antwortet 404:

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

Nicht Teil von v1: das Hochladen eines Belegs (kein Multipart-Endpunkt), die KI-Extraktion und die Drive-Import-Entwürfe in Prüfung, die hier unsichtbar sind. Zu erwartende Codes: 403 SUPPLIER_BILL_LIMIT_REACHED nach Ausschöpfen des monatlichen Ausgabenkontingents, 403 CURRENCY_PRO_ONLY für eine andere Währung als CHF auf einem Free-Plan.

6. Banktransaktionen

Es gibt kein einzelnes POST: Die App erstellt Transaktionen nur im Stapel, eine importierte Datei ist ein Stapel, und der Stapel ist der Einstiegspunkt.

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 enthält 1 bis 100 Zeilen (sonst 400 BATCH_SIZE_INVALID), jede mindestens mit date, amount, currency, label und bankAccount. amount ist vorzeichenbehaftet: eine Gutschrift positiv, eine Belastung negativ. Der Stapel ist eine einzige Datenbanktransaktion, alles wird geschrieben oder nichts. importBatchId gruppiert die Zeilen einer Datei; fehlt es, wird es erzeugt, und die Antwort nennt es. Eine gesendete category wird als manuelle Wahl gespeichert, die die Importheuristik nie überschreibt.

{ "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 ist das Kalenderjahr des Buchungsdatums; ein Geschäftsjahr, das nicht im Dezember schliesst, erstreckt sich über zwei, lassen Sie den Filter weg, um die ganze Historie zu durchblättern. PATCH nimmt category (als manuell markiert) und color, ein #rrggbb-Tag oder "" zum Löschen; DELETE antwortet 204.

7. Jahresabschluss

Der Jahresabschluss ist eine Ressource pro Firma und Geschäftsjahr: fiscalYear ist das Abschlussjahr (das Geschäftsjahr 2025/26 einer Firma mit Abschluss im Juni ist 2026) und dient als Kennung, daher ein GET/PUT-Paar auf dem Jahr statt einer Sammlung.

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

Das Lesen liefert den gespeicherten input, den Schnappschuss ratesUsed und, sobald der Generator der App ihn erzeugt hat, das berechnete Dokument unter snapshot. 404, wenn für dieses Jahr nichts gespeichert ist. Das Schreiben ist ein Upsert auf (Firma, Geschäftsjahr):

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

Der Body trägt die Eingabe des Erzeugungsformulars: die bestätigten Bankkonten mit Eröffnungssaldo, die manuell erfassten Posten (manual), die Abschlusskurse und ein optionales period. Der snapshot ist abgeleitet, vom Generator erzeugt, nie Teil der Anfrage: Ein PUT, der ihn weglässt, lässt das gespeicherte Dokument unberührt. period ist nur für ein Geschäftsjahr nötig, das nicht dem Kalenderjahr entspricht, und sein to muss in fiscalYear fallen (sonst 422 PERIOD_FISCAL_YEAR_MISMATCH).

8. Treuhandmandate

Für einen Treuhänder ist /mandates eine Schlüsselroute ohne Plan oder Modul: Sie listet die Kundenfirmen, die Ihnen gehören. mandateId ist schlicht die companyId des Mandats. Ein Mandat zu erstellen, erstellt eine Firma:

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

Von da an gilt jede Route /companies/{companyId}/… mit derselben Kennung für das Mandat — Mitarbeitende, Lohnabrechnungen und Abwesenheiten eingeschlossen: Derselbe Schlüssel listet die Mitarbeitenden eines Mandats und macht dessen Lohn.

Für die überall gültigen Regeln (Paginierung, Daten, strikte Bodies, Fehlercodes, Sandbox) lesen Sie Konventionen und Fehler; der Fakturierungsweg steht in Rechnungen per API. Jedes Feld ist in der Swagger UI Ihres Deployments beschrieben (…/api/docs).