# Billify API (v1)

Mit der Billify-API verbinden Business-Kunden Billify mit eigenen Systemen: CRM, Buchhaltung, Automatisierungen oder KI-Agenten. Diese Seite beschreibt alles, was die API heute kann.

Dieselbe Dokumentation gibt es als reinen Text, zum Weitergeben an einen KI-Agenten: [https://www.billify.ch/api-dokumentation.md](https://www.billify.ch/api-dokumentation.md)

## Kurz für KI-Agenten

- Basis-URL: `https://www.billify.ch/api/v1`
- Jede Anfrage mit dem Header `Authorization: Bearer billify_sk_…`
- Für API-Schlüssel sind nur die Pfade unter `/api/v1` gedacht. Alle anderen Pfade unter `/api/` gehören zur Web-App, verlangen eine Anmeldung im Browser und antworten auf einen API-Schlüssel immer mit `401`. Diese Pfade nicht verwenden.
- Was auf dieser Seite nicht steht, kann die API nicht. Keine Pfade erraten.
- Beträge, Totale, Nummern und Status setzt Billify selbst. Mitgeschickte Werte dafür werden ignoriert.
- Vor dem Anlegen eines Kunden mit `GET /clients` nach ihm suchen oder auf die Antwort `409` reagieren, sonst entstehen Dubletten.

## Zugang

- Die API gehört zum Business-Plan.
- Schlüssel erstellen: in Billify unter Einstellungen, Team, «API Schlüssel». Das können nur Administratoren der Firma.
- Der Schlüssel wird nur einmal angezeigt. Billify speichert ihn nicht im Klartext und kann ihn nicht erneut anzeigen. Geht er verloren, einen neuen erstellen.
- Ein Schlüssel gilt für die ganze Firma. Er funktioniert nur, solange die Person, die ihn erstellt hat, Administrator der Firma ist. Wird sie entfernt oder zum Mitglied gemacht, hört der Schlüssel auf zu funktionieren.
- Den Schlüssel wie ein Passwort behandeln: nicht in Webseiten-Code, nicht in öffentliche Repositories, nicht in E-Mails. Ein Schlüssel lässt sich in den Einstellungen jederzeit widerrufen.

## Anfragen und Antworten

- Alles in JSON und UTF-8. Bei `POST` den Header `Content-Type: application/json` setzen.
- Erfolg: `{ "data": … }`. Listen enthalten zusätzlich `"count"`.
- Fehler: `{ "error": "…" }`. Die Meldung nennt bei ungültigen Eingaben das betroffene Feld.
- IDs sind ganze Zahlen.
- Beträge sind Dezimalzahlen in der Währung des Dokuments (CHF oder EUR), zum Beispiel `1250.5`. Nicht in Rappen.
- Datum beim Senden: `JJJJ-MM-TT`, zum Beispiel `2026-11-30`. In Antworten: Zeitstempel nach ISO 8601 in UTC, zum Beispiel `2026-11-30T00:00:00.000Z`.
- Antworten können weitere Felder enthalten. Verlässlich sind nur die hier beschriebenen.

| Status | Bedeutung |
|---|---|
| `200` | Erfolg |
| `201` | Angelegt |
| `400` | Eingabe ungültig, Details in `error` |
| `401` | Schlüssel fehlt, ist ungültig oder widerrufen, oder die Firma hat keinen Business-Plan |
| `404` | Nicht gefunden, oder der Eintrag gehört zu einer anderen Firma |
| `409` | Kunde mit diesem Firmennamen existiert bereits |
| `429` | Zu viele Anfragen. Der Header `Retry-After` nennt die Wartezeit in Sekunden |
| `500` | Fehler bei Billify. Später nochmals versuchen |

### Grenzen

- Höchstens 100 Anfragen pro Minute.
- Listen liefern die neuesten 100 Einträge, die neuesten zuerst. Blättern und Filtern gibt es noch nicht.

## Übersicht

| Anfrage | Zweck |
|---|---|
| `GET /clients` | Die neuesten 100 Kunden, ohne gelöschte |
| `GET /clients/{id}` | Ein Kunde |
| `POST /clients` | Kunde anlegen |
| `GET /offers` | Die neuesten 100 Offerten mit Positionen |
| `GET /offers/{id}` | Eine Offerte |
| `POST /offers` | Offerte als Entwurf anlegen |
| `GET /invoices` | Die neuesten 100 Rechnungen mit Positionen |
| `GET /invoices/{id}` | Eine Rechnung |

## Kunden

### Kunden lesen

`GET /clients` und `GET /clients/{id}`

| Feld | Inhalt |
|---|---|
| `id` | ID des Kunden |
| `companyName` | Firmenname |
| `address`, `postalCode`, `city`, `country` | Adresse |
| `mainContactName`, `mainContactEmail`, `mainContactPhone` | Hauptkontakt |
| `industry`, `website` | Branche und Website |
| `language` | Sprache der Dokumente für diesen Kunden: `de`, `en`, `fr` oder `it`. Leer heisst Sprache der Firma |
| `notes` | Interne Notizen |
| `createdAt`, `updatedAt` | Angelegt und zuletzt geändert |
| `_count.invoices`, `_count.offers` | Anzahl Rechnungen und Offerten |

### Kunde anlegen

`POST /clients`

```json
{
  "companyName": "Muster Gartenbau GmbH",
  "address": "Seestrasse 1",
  "postalCode": "8500",
  "city": "Frauenfeld",
  "country": "Schweiz",
  "mainContactName": "Anna Muster",
  "mainContactEmail": "anna@muster-gartenbau.ch",
  "mainContactPhone": "+41 52 000 00 00",
  "language": "de",
  "notes": "Kontakt über Messe"
}
```

| Feld | Pflicht | Hinweis |
|---|---|---|
| `companyName` | ja | höchstens 200 Zeichen |
| `address` | nein | höchstens 300 Zeichen |
| `postalCode` | nein | höchstens 20 Zeichen |
| `city`, `country` | nein | je höchstens 100 Zeichen |
| `mainContactName` | nein | höchstens 200 Zeichen |
| `mainContactEmail` | nein | gültige E-Mail-Adresse |
| `mainContactPhone` | nein | höchstens 50 Zeichen |
| `industry` | nein | höchstens 100 Zeichen |
| `website` | nein | höchstens 500 Zeichen |
| `language` | nein | `de`, `en`, `fr` oder `it` |
| `notes` | nein | höchstens 5000 Zeichen |

Antwort `201` mit dem neuen Kunden in `data`.

Gibt es schon einen Kunden mit demselben Firmennamen (Gross- und Kleinschreibung egal), antwortet die API mit `409` und liefert den bestehenden Kunden in `data`. So entsteht bei einem zweiten Versuch keine Dublette. Wer trotzdem einen zweiten Kunden mit gleichem Namen will: `POST /clients?allowDuplicate=true`.

## Offerten

### Offerten lesen

`GET /offers` und `GET /offers/{id}`

| Feld | Inhalt |
|---|---|
| `id` | ID der Offerte |
| `offerNumber` | Offertnummer, zum Beispiel `OF-00042` |
| `title` | Titel |
| `status` | `DRAFT` Entwurf, `SENT` versendet, `ACCEPTED` angenommen, `REJECTED` abgelehnt, `EXPIRED` abgelaufen, `CONVERTED` in eine Rechnung umgewandelt |
| `offerDate`, `validUntil` | Offertdatum und gültig bis |
| `amountNet` | Betrag nach Rabatt, ohne MWST |
| `amountGross` | Betrag inklusive MWST |
| `vatRate` | MWST-Satz in Prozent |
| `discount`, `discountType` | Rabatt, `PERCENT` oder `AMOUNT` |
| `currency` | `CHF` oder `EUR` |
| `billingType`, `billingMonths` | `ONE_TIME` einmalig oder `MONTHLY` monatlich, mit Anzahl Monate |
| `language` | Sprache des Dokuments |
| `notes` | Bemerkungen auf der Offerte |
| `emailSentAt` | Wann die Offerte per E-Mail versendet wurde |
| `acceptedAt`, `declinedAt`, `signedByName` | Annahme oder Ablehnung durch den Kunden, Name der unterzeichnenden Person |
| `clientId`, `client` | Kunde |
| `items` | Positionen, siehe unten |

Jede Position in `items`:

| Feld | Inhalt |
|---|---|
| `position` | Reihenfolge, ab 1 |
| `description` | Bezeichnung |
| `details` | Beschreibung unter der Bezeichnung |
| `quantity` | Menge |
| `unit` | Einheit, zum Beispiel `Stk`, `Std.` oder `Pauschale` |
| `unitPrice` | Preis pro Einheit, ohne MWST |
| `totalPrice` | Menge mal Preis, ohne MWST |

### Offerte anlegen

`POST /offers`

```json
{
  "clientId": 3,
  "title": "Gartenpflege 2027",
  "offerDate": "2026-10-07",
  "validUntil": "2026-11-30",
  "items": [
    { "description": "Grundpflege Garten", "details": "Rasen, Hecken, Beete", "quantity": 12, "unit": "Monat", "unitPrice": 180 },
    { "description": "Baumschnitt", "quantity": 6.5, "unit": "Std.", "unitPrice": 95 }
  ],
  "vatRate": 8.1,
  "discount": 5,
  "discountType": "PERCENT",
  "notes": "Preise exkl. Entsorgung.",
  "language": "de",
  "currency": "CHF"
}
```

| Feld | Pflicht | Hinweis |
|---|---|---|
| `clientId` | ja | ID eines Kunden der eigenen Firma |
| `title` | ja | höchstens 200 Zeichen |
| `items` | ja | 1 bis 200 Positionen |
| `items[].description` | ja | höchstens 500 Zeichen |
| `items[].unitPrice` | ja | Preis pro Einheit ohne MWST |
| `items[].quantity` | nein | ohne Angabe 1 |
| `items[].unit` | nein | ohne Angabe `Stk` |
| `items[].details` | nein | höchstens 5000 Zeichen |
| `offerDate` | nein | `JJJJ-MM-TT`, ohne Angabe heute |
| `validUntil` | nein | `JJJJ-MM-TT` |
| `vatRate` | nein | `8.1`, `2.6`, `3.8` oder `0`. Ohne Angabe `8.1`, für Firmen ohne MWST-Pflicht `0` |
| `discount` | nein | nur zusammen mit `discountType` |
| `discountType` | nein | `PERCENT` (höchstens 100) oder `AMOUNT` |
| `notes` | nein | Bemerkungen auf der Offerte |
| `language` | nein | `de`, `en`, `fr` oder `it` |
| `currency` | nein | `CHF` oder `EUR`, ohne Angabe `CHF` |
| `billingType` | nein | `ONE_TIME` oder `MONTHLY` |
| `billingMonths` | nein | 1 bis 120, nur bei `MONTHLY` |

- Die Offerte entsteht immer als **Entwurf**. Die Offertnummer vergibt der Nummernkreis der Firma. Versendet wird in Billify, nach einem Blick auf die Vorschau.
- Positionsbeträge und Totale rechnet Billify selbst, gleich wie im Formular und im PDF.

Antwort `201` mit der Offerte samt Positionen. Zusätzlich enthält sie `appUrl`, den Link zur Offerte in Billify.

## Rechnungen

### Rechnungen lesen

`GET /invoices` und `GET /invoices/{id}`

| Feld | Inhalt |
|---|---|
| `id` | ID der Rechnung |
| `invoiceNumber` | Rechnungsnummer |
| `title` | Titel |
| `status` | `DRAFT` Entwurf, `OPEN` offen, `PARTIALLY_PAID` teilweise bezahlt, `PAID` bezahlt, `OVERDUE` überfällig, `CANCELLED` storniert |
| `invoiceDate`, `dueDate` | Rechnungsdatum und fällig am |
| `paidDate` | Bezahlt am |
| `amountNet` | Betrag nach Rabatt, ohne MWST |
| `amountGross` | Betrag inklusive MWST |
| `vatRate` | MWST-Satz in Prozent |
| `discount`, `discountType` | Rabatt, `PERCENT` oder `AMOUNT` |
| `currency` | `CHF` oder `EUR` |
| `qrReference` | QR-Referenz des Zahlteils |
| `language` | Sprache des Dokuments |
| `notes` | Bemerkungen auf der Rechnung |
| `emailSentAt` | Wann die Rechnung per E-Mail versendet wurde |
| `reminderCount` | Anzahl versendeter Mahnungen |
| `cancelledAt`, `cancelReason` | Storno mit Grund |
| `clientId`, `client` | Kunde |
| `items` | Positionen, gleich aufgebaut wie bei den Offerten |

## Beispiele

Den Schlüssel in einer Umgebungsvariable halten, nicht im Befehl selbst:

```bash
export BILLIFY_API_KEY="billify_sk_…"
```

Kunden lesen:

```bash
curl https://www.billify.ch/api/v1/clients \
  -H "Authorization: Bearer $BILLIFY_API_KEY"
```

Kunden anlegen:

```bash
curl -X POST https://www.billify.ch/api/v1/clients \
  -H "Authorization: Bearer $BILLIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"companyName": "Muster Gartenbau GmbH", "city": "Frauenfeld", "mainContactEmail": "anna@muster-gartenbau.ch"}'
```

Offerte anlegen:

```bash
curl -X POST https://www.billify.ch/api/v1/offers \
  -H "Authorization: Bearer $BILLIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"clientId": 3, "title": "Gartenpflege 2027", "items": [{"description": "Grundpflege Garten", "quantity": 12, "unit": "Monat", "unitPrice": 180}]}'
```

Eine Rechnung lesen:

```bash
curl https://www.billify.ch/api/v1/invoices/128 \
  -H "Authorization: Bearer $BILLIFY_API_KEY"
```

## Was die API noch nicht kann

- Rechnungen anlegen
- PDFs von Rechnungen oder Offerten abrufen
- Rechnungen oder Offerten versenden
- Kunden, Offerten oder Rechnungen ändern oder löschen

Das alles geht in der Web-App. Wer eine dieser Funktionen über die API braucht, meldet sich unter info@billify.ch.

## Nachvollziehbarkeit

Jeder über die API angelegte Kunde und jede Offerte steht im Audit-Log der Firma, mit dem Namen des verwendeten Schlüssels.

## Stabilität

Pfade und Felder unter `/api/v1` bleiben bestehen. Neue Felder und neue Pfade können dazukommen. Änderungen, die bestehende Anbindungen brechen würden, kommen nur unter einer neuen Version.

### Änderungen

- Oktober 2026: Kunden und Offerten über die API anlegen (`POST /clients`, `POST /offers`).
- Davor: nur lesen.
