Zurück zur Anleitung

Bill Alps API: Konventionen und Fehler

Was auf jeder Route gilt: Hülle, Paginierung, Daten, strikte Bodies, Fehler, Nutzung, und was ein Sandbox-Reset mit Ihren Schlüsseln macht.

Entwickler9 Min. Lesezeit
Inhaltsverzeichnis+

Die anderen Anleitungen gehen Abläufe durch; diese sammelt die Regeln, die für jeden Aufruf gelten, zum Bereithalten, während Sie Ihren Client schreiben.

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

1. Die data-Hülle

Jede erfolgreiche Antwort packt ihre Nutzlast in data:

{ "data": [ ... ] }

Zwei Ausnahmen ohne Body: 204 No Content beim Löschen und bei einigen Aktionen (PATCH …/payslips/{payslipId}, POST …/payslips/{payslipId}/send, POST …/quotes/{quoteId}/send, POST …/invoices/{invoiceId}/cancel-payment) sowie 302 beim Herunterladen von PDFs, Belegen und Logos, mit -L zu folgen.

2. Paginierung

Listen von Firmenressourcen sind paginiert, die neuesten zuerst (Mitarbeitende folgen ihrer Erstellungsreihenfolge):

{ "data": [ ... ], "nextCursor": "..." }

Einige Listen sind nicht paginiert und antworten mit einem einfachen Array unter data: GET /companies, GET /mandates, GET /companies/{companyId}/invoice-templates, GET …/payroll/holiday-variants und GET …/employees/{employeeId}/payslips-to-update. POST …/payslips/pdf-batch ist ebenfalls paginiert, aber sein Cursor reist im Body, als cursor.

3. Daten und Kennungen

Datumsangaben werden als Epoch-Millisekunden oder YYYY-MM-DD angenommen und als Epoch-Millisekunden zurückgegeben. Die Filter from/to begrenzen das Ausstellungsdatum, beide Enden eingeschlossen. Zwei Ausnahmen: Absenzen und die Daten eines Mitarbeitenden (startDate, endDate, dateOfBirth) sind YYYY-MM-DD-Strings, hin und zurück.

Kennungen sind undurchsichtige Strings. Im Pfad antworten eine unbekannte, eine fehlerhafte und eine Kennung einer anderen Firma alle 404 NOT_FOUND: Die drei Fälle sind bewusst nicht unterscheidbar, damit keine Kennung eines anderen Kontos durchsickert. Im Body antwortet ein Verweis auf eine andere Firma je nach Route: 403 ACCESS_DENIED bei einer Rechnung oder Offerte, 422 CLIENT_NOT_FOUND oder 422 TEMPLATE_NOT_FOUND bei einer wiederkehrenden Rechnung, 422 EMPLOYEE_NOT_FOUND bei einer Absenz.

4. Strikte Bodies

  • Die JSON-Bodies sind strikt: Jeder unbekannte Schlüssel antwortet 400 VALIDATION_ERROR. Beim Erstellen und Ändern eines Mitarbeitenden und beim Erstellen einer Lohnabrechnung kommt die Ablehnung aus der serverseitigen Prüfung und nennt das Feld in message, ohne issues. Die Bodies von …/laa-periods und …/avs-choices ignorieren dagegen unbekannte Schlüssel.
  • Ein Body ist bei POST, PUT und PATCH immer nötig ({} ist ein gültiger leerer Patch; ein leerer Body ist INVALID_JSON), ausser bei Aktionen ohne Body wie POST …/default, …/convert, …/cancel-payment oder …/payslips/{payslipId}/send; DELETE nimmt keinen. Achtung: POST …/payslips/{payslipId}/validate erwartet mindestens {}.
  • Über 1 MB wird der Body abgelehnt: 413 PAYLOAD_TOO_LARGE.
  • Sieben Routen nehmen rohe Bytes statt JSON entgegen. POST …/expenses/{expenseId}/document, POST …/expense-drafts/extract, POST …/uploads/expense und POST …/uploads/invoice-import: PDF, PNG, JPEG oder WebP, 10 MB. POST …/uploads/invoice-source: PNG, JPEG oder WebP, 8 MB. PUT …/logo auf einer Firma und auf einer Rechnungsvorlage: nur PNG oder JPEG, 2 MB. Der Content-Type nennt das Format, der Body ist die Datei; alles Übrige reist im Query-String. Ein anderes Format antwortet 415 UNSUPPORTED_MEDIA_TYPE, ein leerer Body 400 EMPTY_BODY.
  • Ein Upload unter …/uploads/… liefert eine uploadId, die eine Stunde gültig ist. Heute verbraucht nur POST …/expense-drafts eine solche: Keine öffentliche Route akzeptiert die von invoice-source und invoice-import.
  • Ein unbekannter Query-Schlüssel ist ein 400 VALIDATION_ERROR auf jeder Route, die einen Query-Vertrag deklariert; die übrigen, wie GET /me und GET /companies, ignorieren den Rest, ohne dass das ein Versprechen wäre.
  • PUT auf einer Absenz ist ein vollständiges Ersetzen: ausgelassene optionale Felder werden gelöscht. PUT auf einer Rechnung oder Offerte ersetzt alle Positionen und verlangt expectedVersion.

Ein Validierungsfehler listet jedes fehlerhafte Feld unter issues, als gepunktete Pfade, Array-Indizes eingeschlossen:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request body",
    "issues": [
      { "path": "subtotal", "message": "Unrecognized key \"subtotal\"" },
      { "path": "vatRates.0.rate", "message": "Too big: expected number to be <=100" }
    ]
  }
}

5. Fehler

Fehler tragen einen stabilen, maschinenlesbaren Code:

{ "error": { "code": "PAYSLIP_ALREADY_EXISTS" } }

VALIDATION_ERROR fügt issues hinzu, oder eine message für serverseitig geprüfte Felder. INVALID_TRANSITION, BATCH_SIZE_INVALID und UNSUPPORTED_MEDIA_TYPE fügen ebenfalls eine message hinzu, und PAYROLL_CUMULATIVE_INCOMPLETE ein details.months; jeder andere Code antwortet allein. Verzweigen Sie auf error.code, nie auf Prosa. Diese Hülle gilt auf jedem Pfad unter /api/v1/, auch bei unbekanntem Pfad (404 NOT_FOUND) und nicht unterstützter Methode (405 METHOD_NOT_ALLOWED, mit Allow-Header).

Die Prüfungen laufen in dieser Reihenfolge: Pfad und Methode (404, 405), Grösse des Bodys (413), Schlüssel und Inhaber (401), Sichtbarkeit der Firma (404), dann archivierte FirmaPlanModulBerechtigung (403). Danach folgen die Validierung von Body und Anfrage (400) und schliesslich die Geschäftsregeln. Eine Firma, bei der der Schlüsselinhaber nicht Mitglied ist, ist von einer nicht existierenden nicht zu unterscheiden.

  • 400 INVALID_JSON, 400 VALIDATION_ERROR: Body fehlt oder ist unlesbar, ungültiges Feld. 400 BATCH_SIZE_INVALID: payslips/batch oder bank-transactions/batch ausserhalb von 1 bis 100 Elementen; die übrigen Stapel sind durch ihr Schema begrenzt und antworten VALIDATION_ERROR.
  • 401 INVALID_API_KEY: Schlüssel fehlt, fehlerhaft, unbekannt, widerrufen oder abgelaufen; Inhaber gesperrt oder gelöscht.
  • 402 SUBSCRIPTION_REQUIRED: Erstellen eines Mandats ohne aktives Treuhand-Abonnement.
  • 403 PLAN_NOT_ALLOWED, 403 COMPANY_ARCHIVED, 403 ACCESS_DENIED, 403 ACCOUNT_DISABLED: Plan, Archivierung, Berechtigung oder Rolle fehlen, Konto deaktiviert.
  • 403 INVOICING_MODULE_DISABLED, 403 PAYROLL_MODULE_DISABLED, 403 ACCOUNTING_MODULE_DISABLED, 403 PRODUCT_CATALOG_DISABLED: Modul oder Katalog in der Firma deaktiviert. 403 LEDGER_DISABLED, 403 FINANCIAL_STATEMENTS_REPORTING_DISABLED: Funktion im Deployment geschlossen.
  • 403 *_LIMIT_REACHED: Plankontingent erreicht (Kunden, Produkte, Produktlisten, Rechnungen, wiederkehrende Rechnungen, Mitarbeitende, Lohnabrechnungen, Ausgaben). 403 EXPENSE_AI_QUOTA_REACHED, 403 AI_QUOTA_REACHED: KI-Kontingente. 403 CURRENCY_PRO_ONLY: andere Währung als CHF bei den Starter-Limiten.
  • 403 FILE_ACCESS_DENIED, 403 UPLOAD_EXPIRED: uploadId eines anderen Benutzers, bereits verwendet oder abgelaufen.
  • 409 INVALID_TRANSITION, 409 PAYSLIP_ALREADY_EXISTS, 409 PAYSLIP_DECLARED, 409 ABSENCE_OVERLAPS_EXISTING, 409 QUOTE_NOT_EDITABLE, 409 BILL_PAID_LOCKED, 409 BILL_NOT_PAID, 409 DOCUMENT_ALREADY_ATTACHED, 409 EXPENSE_DRAFT_LIMIT_REACHED: Konflikt mit dem aktuellen Zustand. 409 BILL_VERSION_CONFLICT, 409 QUOTE_VERSION_CONFLICT: Das Dokument hat sich seit Ihrer Abfrage geändert.
  • 422 MODIFICATION_REASON_REQUIRED, 422 REASON_REQUIRED, 422 CLIENT_EMAIL_MISSING, 422 EMPLOYEE_EMAIL_MISSING, 422 CLASSIFICATION_ACCOUNT_REQUIRED, 422 VALID_UNTIL_IN_PAST, 422 SCAN_UNREADABLE: gültige Anfrage, in diesem Kontext aber unzulässig.
  • 429 AI_RATE_LIMITED: Das KI-Modell begrenzt die Aufrufe, erneut versuchen. 429 MANDATE_REQUEST_LIMIT_REACHED: zu viele Mandatsanfragen.
  • 503 PDF_UNAVAILABLE, 502 EMAIL_SEND_FAILED, 502 AI_REQUEST_FAILED, 500 INTERNAL_ERROR: erneut versuchen. Eine einzige Ausnahme: Nach 502 QUOTE_EMAIL_SENT_TRANSITION_FAILED ist die Offerte bereits raus, senden Sie sie nicht erneut.

Die Swagger UI und openapi.json Ihres Deployments veröffentlichen die deklarierten Codes, Route für Route. Weitere fachliche Codes bleiben möglich: Sehen Sie auf error.code immer einen Standardfall vor.

6. Limiten und Nutzung

Es gibt noch keine Ratenbegrenzung. Aufrufe werden gezählt, nicht gebremst: ein Zähler pro Schlüssel, Endpunkt, Zielfirma und UTC-Tag, sichtbar unter Mein KontoAPI-SchlüsselNutzung. Von der Authentifizierung abgelehnte Anfragen werden nicht gezählt; ein 400 auf einem autorisierten Aufruf schon.

Seien Sie trotzdem ein guter Nachbar: Blättern Sie durch Listen, statt sie in einer Schleife abzufragen, und ziehen Sie sich bei 500 oder 503 zurück. Nicht alle Stapel verhalten sich gleich. POST …/payslips/batch ist nicht atomar und liefert ein Ergebnis pro Element. POST …/bank-transactions/batch, POST …/clients/batch und POST …/{invoices,quotes,payslips}/bulk-delete sind atomar, alles oder nichts. Die Routen bulk-* der Ausgaben und der Banktransaktionen sowie …/expense-drafts/accept und …/reject verarbeiten, was sie können, und zählen den Rest in skipped.

7. Sandbox und Reset

Die Sandbox schreibt nie in die Produktion. E-Mails, Stripe und Team-Einladungen sind dort inert, die Registrierung ist geschlossen, Logos werden nicht kopiert. Alles andere läuft, auch die geplanten Jobs: Wiederkehrende Rechnungen werden erzeugt und Lohnabrechnungen auf den kopierten Daten berechnet. Das Hauptbuch ist dort geöffnet, anders als in der Produktion, wo seine Routen 403 LEDGER_DISABLED antworten.

Zurück zu den Abläufen: Erste Schritte mit der API, Rechnungen per API, Lohn, Ausgaben und Buchhaltung per API, Lohnlauf per API. Jedes Feld und jede Antwort stehen in der Swagger UI Ihres Deployments (…/api/docs).