Zurück zur Anleitung

Lohn, Ausgaben und Buchhaltung per API

Die Mitarbeitenden, das Erstellen der Lohnabrechnungen, die Abwesenheiten, die Ausgaben, die Kontoauszüge und die Jahresrechnung, in REST-Aufrufen.

Entwickler14 Min. Lesezeit
Inhaltsverzeichnis+

Diese Anleitung deckt alles ab, was nicht Fakturierung ist, ausser dem monatlichen Ablauf der Lohnabrechnungen, der in Lohnlauf per API ausführlich beschrieben ist. Derselbe Schlüssel, dieselbe Basis-URL wie in Erste Schritte mit der API:

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

1. Eine Firma ansprechen

Die Lohnrouten werden wie alle anderen Ressourcen über die Firma adressiert: /companies/{companyId}/employees und …/payslips. Mit demselben Schlüssel rechnen Sie den Lohn aller Firmen ab, 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 Feld 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 Abrechnungen der Person neu, die noch Entwürfe sind und den laufenden oder einen späteren Monat betreffen, 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.

DELETE …/employees/{employeeId} antwortet 204 und löscht auch die Lohnabrechnungen, die Abwesenheiten und die Links zum Mitarbeiterbereich der Person. Es wird abgelehnt (409 PAYSLIP_DECLARED), wenn eine ihrer Abrechnungen gemeldet ist.

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, die neuesten 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. hoursWorked ist Pflicht für Mitarbeitende im Stundenlohn. 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

Die Vorschauen, die Validierung, der Versand, die Korrektur, das Duplizieren und die PDFs im Stapel sind in Lohnlauf per API ausführlich beschrieben.

4. Abwesenheiten

Abwesenheiten sind firmenbezogen (/companies/{companyId}/absences, Lohnmodul). 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 '{ "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 und total sind Pflicht; die MWST-Aufschlüsselung (subtotal, vatRate, vatAmount) und die Kennungen des Lieferanten sind optional. status ist toPay oder paid. kind ist supplierInvoice, expenseReport, vatReturn, taxAssessment, socialStatement, shareholderResolution, financingContract oder other. taxAssessment und socialStatement verlangen eine documentNumber, die die Art oder das Regime und die Periode angibt.

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. Die Liste liefert standardmässig die Lieferantenrechnungen, mit ?kind= wählen Sie einen anderen Typ. 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 Beleg wird mit derselben 302-Weiterleitung wie die PDFs heruntergeladen; eine Ausgabe ohne Beleg (hasDocument: false) antwortet 404:

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

Um einen Beleg anzuhängen, senden Sie die Datei selbst als Body der Anfrage, ohne Multipart (PDF, PNG, JPEG oder WebP, höchstens 10 MB). Das geht nur einmal: Eine Ausgabe, die bereits einen Beleg hat, antwortet 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 und POST …/expenses/bulk-delete verarbeiten 1 bis 100 Kennungen und antworten affected und skipped: Eine unbekannte Kennung wird in skipped gezählt, ohne die übrigen zu blockieren. Deckt Ihr Abonnement das Modul Buchhaltung nicht ab, rechnen Sie ausserdem mit 403 SUPPLIER_BILL_LIMIT_REACHED nach Ausschöpfen des monatlichen Ausgabenkontingents und mit 403 CURRENCY_PRO_ONLY für eine andere Währung als CHF.

Entwürfe für Ausgaben

Ein Entwurf ist eine Ausgabe, die auf ihre Prüfung wartet: Er erscheint nicht in GET …/expenses und verbraucht das Ausgabenkontingent erst, wenn er angenommen wird. Eine Firma kann höchstens 100 ausstehende Entwürfe haben, den Google-Drive-Import eingeschlossen (409 EXPENSE_DRAFT_LIMIT_REACHED). Es gibt zwei Wege, Entwürfe zu erstellen:

# Dokument per KI auslesen, in einem Aufruf
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/pdf" \
  --data-binary @receipt.pdf "$BASE/companies/$COMPANY/expense-drafts/extract?locale=de&filename=receipt.pdf"

# Ohne KI: Datei hochladen, dann den Entwurf mit eigenen Werten erstellen
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 antwortet 201 mit draft, readabilityScore und quota. Jedes erfolgreiche Auslesen verbraucht eine Einheit des monatlichen Extraktionskontingents (200 in Business), ein fehlgeschlagenes Auslesen nicht. Zu erwartende Codes: 403 EXPENSE_AI_QUOTA_REACHED, 422 SCAN_UNREADABLE (senden Sie einen besseren Scan), 429 AI_RATE_LIMITED und 502 AI_REQUEST_FAILED (erneut versuchen).

…/uploads/expense liefert eine uploadId, die eine Stunde gültig ist. POST …/expense-drafts verbraucht sie, ohne KI und ohne Extraktionskontingent, und DELETE …/uploads/expense/{uploadId} verwirft eine nicht verwendete Datei. Eine abgelaufene Kennung antwortet 403 UPLOAD_EXPIRED, eine von einem anderen Benutzer oder für eine andere Firma erstellte Kennung 403 FILE_ACCESS_DENIED.

Für die Prüfung: GET …/expense-drafts (paginiert), GET …/expense-drafts/{draftId}, GET …/expense-drafts/{draftId}/document für die Datei und PATCH …/expense-drafts/{draftId}, um ein Feld zu korrigieren. Danach nehmen Sie die Entwürfe an oder lehnen sie ab:

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

…/accept und …/reject nehmen 1 bis 100 ids entgegen und antworten affected und skipped. Eine unbekannte Kennung wird in skipped gezählt, eine Lieferantenrechnung ohne Konto lässt dagegen den ganzen Aufruf mit 422 CLASSIFICATION_ACCOUNT_REQUIRED scheitern: Korrigieren Sie sie zuerst mit PATCH. Das Ablehnen ist endgültig, die Datei verschwindet mit dem Entwurf.

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": "Miete Januar", "bankAccount": "CH93 0076 2011 6238 5295 7",
            "bankReference": "CH9300762011623852957:2026-0001" }
        ] }' \
  $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: Ein Zahlungseingang ist 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 accountId kontiert die Zeile wie eine manuelle Wahl, die die Importheuristik nie überschreibt.

{ "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": "Miete prüfen", "color": "#3c8fc3" }' \
  $BASE/companies/$COMPANY/bank-transactions/<transactionId>

year ist das Kalenderjahr des Buchungsdatums (date); 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 die Felder der Zeile entgegen, dazu color (ein Farbwert #rrggbb oder "" zum Löschen) und accountId (null entfernt die Kontierung); DELETE antwortet 204.

Für mehrere Zeilen: POST …/bank-transactions/bulk-delete, …/bulk-color und …/bulk-account nehmen 1 bis 100 ids entgegen und antworten affected und skipped; …/bulk-accounts nimmt Paare { id, accountId } entgegen. …/matching/delete und …/matching/account wirken auf das, was ein Filter innerhalb eines Jahres auswählt (year ist Pflicht), und zwar in Etappen: Wiederholen Sie denselben Body, solange done den Wert false hat. …/categorize lässt 1 bis 100 Zeilen per KI kontieren und verbraucht das monatliche Kontingent für KI-Vorausfüllungen (100 in Business).

7. Jahresrechnung

Die Jahresrechnung ist eine schreibgeschützte Ressource pro Firma und Geschäftsjahr: fiscalYear ist das Abschlussjahr (das Geschäftsjahr 2025/26 einer Firma mit Abschluss im Juni ist 2026). Die API liefert die Version, die die Firma in der App festgestellt hat, sobald das Hauptbuch das Geschäftsjahr abgeschlossen hat.

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

Das Lesen liefert die gültige Version so, wie ihr PDF sie druckt: die Zeilen balanceSheet und incomeStatement mit ihren eingefrorenen Bezeichnungen und den Beträgen des Geschäfts- und des Vorjahres, den Antrag über die Gewinnverwendung appropriation (AG, GmbH) oder die Ergebnisverteilung partnerAllocation (Personengesellschaften), dazu die Nummer der version, die language des Dokuments und den pdfStatus. 404, solange die Jahresrechnung nicht festgestellt ist, und erneut nach einer Wiedereröffnung des Geschäftsjahres.

8. Treuhandmandate

Für ein Treuhandbüro ist /mandates eine Schlüsselroute, ohne Plan und ohne Modul. GET /mandates listet Ihre Mandate ohne Paginierung auf: die Firmen, die Ihnen gehören (kind: owned, id gleich der companyId), dann Ihre Anfragen und Zugriffe auf Firmen, die ein anderes Konto verwaltet (kind: external, id der Anfrage, companyId der Firma). ?includeClosed=true fügt die archivierten Mandate und die abgeschlossenen Anfragen hinzu. 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

Die Antwort 201 liefert das Mandat zurück. Der Body nimmt name (mit optionalem ide), ide allein, wenn die Firma schon auf Bill Alps ist, oder companyId allein. Eine Firma, die bereits einem anderen Konto gehört, wird nicht dupliziert: Die Antwort ist 202, mit einer Anfrage im Status pending, und deren Administratoren erhalten eine E-Mail. Zu erwarten: 402 SUBSCRIPTION_REQUIRED ohne aktives Treuhand-Abonnement, 403 FIDUCIAIRE_ONLY bei einem Konto, das nicht zu einem Treuhandbüro gehört, 409 IDE_ALREADY_USED und 422 INVALID_IDE bei der UID.

Vier Aktionen ohne Body ergänzen die Liste, unter /mandates/{mandateId}/…: archive und restore für ein Mandat, das Ihnen gehört, cancel, um eine ausstehende Anfrage zurückzuziehen, leave, um einen angenommenen Zugriff zurückzugeben.

Von da an gilt jede Route /companies/{companyId}/… für das Mandat, mit seiner companyId, Mitarbeitende, Lohnabrechnungen und Abwesenheiten eingeschlossen, solange der plan des Mandats business ist: Derselbe Schlüssel listet die Mitarbeitenden eines Mandats auf und rechnet deren Löhne ab.

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, der Lohnmonat in Lohnlauf per API. Jedes Feld ist in der Swagger UI Ihres Deployments beschrieben (…/api/docs).