Pfeffersack REST API
Programmatischer Zugriff auf Rechnungen, Belege und Buchungen Ihres Pfeffersack-Accounts. Die Endpoints sind nach Sektion getrennt — GmbHs nutzen die doppelte Buchführung (Journal), Einzelunternehmen die vereinfachte Buchungs-Tabelle. Aktuell unterstützt die API ausschließlich Lesezugriff.
Quickstart
- API-Key erstellen unter Einstellungen → Integrationen → API-Keys. Der Key wird nur einmal angezeigt.
- Den Key im
Authorization-Header jedes Requests mitsenden. - Alle Responses sind JSON mit
Content-Type: application/json; charset=utf-8.
Erster Request
curl -H "Authorization: Bearer pfs_live_IHR_KEY" \
"https://app.pfeffersack.ch/api/v1/gmbh/invoices?limit=5"Authentifizierung
Die API nutzt Bearer-Token-Authentifizierung. Der Key wird einmalig bei der Erstellung angezeigt — speichern Sie ihn an einem sicheren Ort (Passwort-Manager oder Secrets- Store Ihres Deployment-Systems).
Authorization: Bearer pfs_live_AbCdEf0123456789...Key-Format
| Prefix | Typ | Beschreibung |
|---|---|---|
| pfs_live_ | string | Produktiv-Key mit vollem Lesezugriff. |
| pfs_test_ | string | Reserviert für zukünftigen Test-Mode (noch nicht aktiv). |
Zeichensatz nach Prefix: A–Z a–z 0–9 _ (Base62 + Underscore). Länge: 32+ Zeichen. Keys werden serverseitig nur als SHA-256-Hash gespeichert.
Fehlgeschlagene Authentifizierung
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="Pfeffersack API"
Content-Type: application/json
{ "success": false, "error": "Unauthorized" }Antworten sind bewusst uniform (gleicher Shape, gleiche Latenz) — das verhindert User-Enumeration und Timing-Attacks.
Konventionen
- Base-URL:
https://app.pfeffersack.ch/api/v1— ausschließlich HTTPS. Plain-HTTP-Requests werden abgewiesen. - Timestamps: ISO-8601 in UTC mit
Z-Suffix (z. B.2026-04-02T14:21:00.000Z) für Datetime-Felder,YYYY-MM-DDfür reine Datumsfelder. - Geldbeträge: JSON
numbermit bis zu 2 Nachkommastellen (z. B.1621.50). Keine Minor-Units. Null = kein Wert,0= explizit null. - Währung: ISO-4217 (meist
CHF, optionalEUR,USD). - IDs: Integer, auto-incrementiert pro User- Scope. IDs sind innerhalb Ihres Accounts eindeutig, aber nicht global.
- Sortierung: Listen sind absteigend nach Datum + ID sortiert (neueste zuerst). Reihenfolge ist stabil über Pages hinweg.
- CORS: Die API ist für Server-to-Server gedacht. Browser-Aufrufe von Drittdomains sind nicht erlaubt — der API-Key würde sonst im Client-Bundle leaken.
- Unbekannte Felder: Ignorieren. Neue Felder können additiv hinzukommen und gelten nicht als Breaking Change.
Rate Limits
Pro API-Key: 120 Requests pro Minute (rollendes Fenster). Bei Überschreitung 429 Too Many Requests mit Retry-After (Sekunden bis Reset).
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Reset: 1713540000
Content-Type: application/json
{ "success": false, "error": "Zu viele Anfragen. Bitte versuchen Sie es später erneut." }Zusätzlich gilt ein IP-basierter Vorab-Limit von 30 Requests pro Minute für unautorisierte Anfragen (fehlender/ungültiger Key). Das schützt vor Brute-Force auf den Key-Lookup.
Empfehlung: Implementieren Sie exponentielles Backoff mit Jitter. Respektieren Sie Retry-After strikt — Requests während der Sperrzeit zählen in den nächsten Window- Zähler.
Pagination
Alle List-Endpoints sind paginiert. Response-Body enthält strukturierte Pagination-Metadaten im Feld pagination.
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| page | integer | 1 | 1-basierte Seitennummer. |
| limit | integer (1–100) | 25 | Einträge pro Seite. Werte außerhalb des Bereichs werden automatisch geclampt. |
{
"success": true,
"data": [ /* ... */ ],
"pagination": {
"page": 1,
"limit": 25,
"total": 142,
"total_pages": 6,
"has_next": true,
"has_prev": false
}
}Fehler-Format
Alle Fehler folgen demselben JSON-Envelope — success=false plus lokalisierte error-Meldung.
{ "success": false, "error": "Rechnung nicht gefunden" }Fehlermeldungen sind menschenlesbar (Deutsch) und können sich ändern. Unterscheiden Sie Fehlerfälle über den HTTP-Status, nicht über den Text.
Status Codes
| Code | Typ | Beschreibung |
|---|---|---|
| 200 OK | success | Request erfolgreich, Response-Body enthält Daten. |
| 400 Bad Request | client | Ungültige Eingabe (z. B. Route-Parameter keine positive Ganzzahl). |
| 401 Unauthorized | client | Kein, ungültiger oder widerrufener API-Key. Header prüfen. |
| 403 Forbidden | client | Key existiert, hat aber keinen Zugriff auf diese Ressource (z. B. GmbH-Endpoint mit Einzelfirma-Key). |
| 404 Not Found | client | Ressource existiert nicht oder gehört nicht zu Ihrem Account. |
| 429 Too Many Requests | client | Rate-Limit überschritten. Retry-After respektieren. |
| 500 Internal Server Error | server | Unerwarteter Serverfehler. Mit exponentiellem Backoff retrien. |
GmbH Endpoints
Erfordern einen API-Key eines Accounts mit business_type = "gmbh". Andernfalls 403 Forbidden. Sensible interne Felder (PDF-Blobs, S3-Keys, Creator-IDs) werden grundsätzlich nicht exponiert.
/v1/gmbh/invoices
business_type =gmbhPaginierte Liste von GmbH-Rechnungen (eingehend und ausgehend).
Query-Parameter
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| page | integer | 1 | Siehe Pagination. |
| limit | integer (1–100) | 25 | Siehe Pagination. |
| status | enum | — | draft, sent, paid, partial, overdue, void, cancelled |
| document_type | enum | — | invoice, offerte, proforma, credit_note |
| from | date (YYYY-MM-DD) | — | Filter: invoice_date ≥ from |
| to | date (YYYY-MM-DD) | — | Filter: invoice_date ≤ to |
curl -H "Authorization: Bearer $PFS_KEY" \
"https://app.pfeffersack.ch/api/v1/gmbh/invoices?status=paid&from=2026-01-01"{
"success": true,
"data": [
{
"id": 142,
"invoice_number": "R-2026-001",
"invoice_type": "outgoing",
"document_type": "invoice",
"invoice_date": "2026-03-15",
"due_date": "2026-04-14",
"valid_until": null,
"debtor_id": 23,
"creditor_id": null,
"net_amount": 1500.00,
"vat_amount": 121.50,
"gross_amount": 1621.50,
"paid_amount": 1621.50,
"currency": "CHF",
"vat_treatment": "normal",
"price_type": "net",
"discount_percentage": 0,
"status": "paid",
"reference": "RF18 5390 0754 7034",
"notes": null,
"items": [
{ "description": "Beratung März", "quantity": 10, "unit_price": 150.00, "vat_rate": 8.1 }
],
"sent_at": "2026-03-15T09:12:00.000Z",
"paid_at": "2026-04-02T14:21:00.000Z",
"created_at": "2026-03-15T09:10:00.000Z",
"updated_at": "2026-04-02T14:21:00.000Z"
}
],
"pagination": { "page": 1, "limit": 25, "total": 1, "total_pages": 1, "has_next": false, "has_prev": false }
}/v1/gmbh/invoices/{id}
business_type =gmbhEinzelne Rechnung. 404 wenn die ID nicht existiert oder nicht zu Ihrem Account gehört. 400 wenn {id} keine positive Ganzzahl ist.
/v1/gmbh/belege
business_type =gmbhPaginierte Liste von Belegen. Soft-gelöschte Belege sind standardmäßig ausgeblendet. Die Original-Datei (PDF/Bild) in S3 wird über diese API nicht bereitgestellt.
Query-Parameter
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| page | integer | 1 | Siehe Pagination. |
| limit | integer (1–100) | 25 | Siehe Pagination. |
| kategorie | string (≤50) | — | Belegkategorie (z. B. büromaterial, reisekosten). |
| from | date (YYYY-MM-DD) | — | Filter: beleg_datum ≥ from |
| to | date (YYYY-MM-DD) | — | Filter: beleg_datum ≤ to |
| include_deleted | 1 | omit | — | Bei '1' werden soft-gelöschte Belege mitgeliefert. |
{
"success": true,
"data": [
{
"id": 812,
"beleg_datum": "2026-04-10",
"kategorie": "büromaterial",
"beschreibung": "Toner HP LaserJet",
"betrag": 129.00,
"waehrung": "CHF",
"file_type": "application/pdf",
"file_size_bytes": 184320,
"original_filename": "rechnung-toner.pdf",
"buchung_id": null,
"journal_entry_id": 4421,
"asset_id": null,
"is_deleted": false,
"uploaded_at": "2026-04-10T08:44:11.000Z",
"created_at": "2026-04-10T08:44:11.000Z",
"updated_at": "2026-04-10T08:44:11.000Z"
}
],
"pagination": { "page": 1, "limit": 25, "total": 1, "total_pages": 1, "has_next": false, "has_prev": false }
}/v1/gmbh/belege/{id}
business_type =gmbhEinzelner Beleg. Keine Original-Datei in der Response.
/v1/gmbh/journal
business_type =gmbhPaginierte Liste der Journal-Einträge (Hauptbuch). In der GmbH- Sektion ersetzt das Journal die einfache Buchungstabelle — jeder Eintrag repräsentiert eine doppelte Buchung mit Referenz auf das Geschäftsjahr.
Query-Parameter
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| page | integer | 1 | Siehe Pagination. |
| limit | integer (1–100) | 25 | Siehe Pagination. |
| fiscal_year_id | integer | — | Auf ein Geschäftsjahr filtern. |
| status | enum | — | draft, posted, voided |
| source | string (≤30) | — | Quelle der Buchung, z. B. invoice, bank_import, manual, payroll. |
| from | date (YYYY-MM-DD) | — | Filter: entry_date ≥ from |
| to | date (YYYY-MM-DD) | — | Filter: entry_date ≤ to |
{
"success": true,
"data": [
{
"id": 4421,
"fiscal_year_id": 15,
"entry_number": "2026-00321",
"entry_date": "2026-04-10",
"description": "Toner HP — Bürobedarf",
"reference": null,
"source": "bank_import",
"source_id": 9877,
"status": "posted",
"payment_method": "bank_transfer",
"template_id": null,
"voided_at": null,
"void_reason": null,
"created_at": "2026-04-10T08:44:12.000Z",
"updated_at": "2026-04-10T08:44:12.000Z"
}
],
"pagination": { "page": 1, "limit": 25, "total": 1, "total_pages": 1, "has_next": false, "has_prev": false }
}/v1/gmbh/journal/{id}
business_type =gmbhEinzelner Journal-Eintrag. Interne User-Referenzen (created_by, voided_by) werden nicht exponiert.
Standard (Einzelunternehmen) Endpoints
Für Accounts ohne GmbH-Business-Type. Kein expliziter business_type-Check — jeder gültige Key darf zugreifen, die Daten sind aber user-scoped (kein Cross-Account- Leak).
/v1/standard/invoices
Paginierte Liste von Rechnungen im Einzelunternehmen-Format.
Query-Parameter
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| page | integer | 1 | Siehe Pagination. |
| limit | integer (1–100) | 25 | Siehe Pagination. |
| status | string (≤30) | — | Rechnungsstatus (z. B. draft, sent, paid, overdue). |
| from | date (YYYY-MM-DD) | — | Filter: invoice_date ≥ from |
| to | date (YYYY-MM-DD) | — | Filter: invoice_date ≤ to |
{
"success": true,
"data": [
{
"id": 88,
"invoice_number": "2026-042",
"invoice_date": "2026-04-10",
"due_date": "2026-05-10",
"customer": {
"name": "Muster AG",
"email": "[email protected]",
"address": "Bahnhofstrasse 1",
"zip": "8001",
"city": "Zürich",
"country": "CH",
"vat_number": "CHE-123.456.789 MWST",
"registration_code": null
},
"amount": 950.00,
"net_amount": 881.59,
"vat_amount": 68.41,
"gross_amount": 950.00,
"currency": "CHF",
"vat_enabled": true,
"status": "paid",
"payment_method": "bank_transfer",
"reference": null,
"notes": null,
"items": [ /* ... */ ],
"booking_id": 512,
"created_at": "2026-04-10T10:02:11.000Z",
"updated_at": "2026-04-12T08:30:00.000Z",
"uploaded_at": null
}
],
"pagination": { "page": 1, "limit": 25, "total": 1, "total_pages": 1, "has_next": false, "has_prev": false }
}/v1/standard/invoices/{id}
Einzelne Rechnung. PDF-Binärdaten werden aus Bandbreiten- und Sicherheitsgründen nie in der Response geliefert.
/v1/standard/belege
Paginierte Liste von Belegen. OCR-Rohtext und S3-Keys werden nicht exponiert.
Query-Parameter
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| page | integer | 1 | Siehe Pagination. |
| limit | integer (1–100) | 25 | Siehe Pagination. |
| from | date (YYYY-MM-DD) | — | Filter: beleg_datum ≥ from |
| to | date (YYYY-MM-DD) | — | Filter: beleg_datum ≤ to |
{
"success": true,
"data": [
{
"id": 201,
"beleg_datum": "2026-04-08",
"beschreibung": "SBB Billet Zürich – Bern",
"vendor_name": "SBB",
"betrag": 51.00,
"waehrung": "CHF",
"vat_amount": 3.98,
"vat_rate": 8.1,
"mime_type": "image/jpeg",
"file_size_bytes": 284112,
"original_filename": "sbb-billet.jpg",
"booking_id": 488,
"processing_status": "done",
"created_at": "2026-04-08T19:02:00.000Z",
"updated_at": "2026-04-08T19:02:30.000Z"
}
],
"pagination": { "page": 1, "limit": 25, "total": 1, "total_pages": 1, "has_next": false, "has_prev": false }
}/v1/standard/belege/{id}
/v1/standard/bookings
Paginierte Liste von Buchungen (einfache Kassenbuch-Einträge). Für GmbHs existiert stattdessen das Journal. Interne Matching-Felder (Bank-Fingerprint, Session-ID, presigned Receipt-URL) werden nicht exponiert.
Query-Parameter
| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| page | integer | 1 | Siehe Pagination. |
| limit | integer (1–100) | 25 | Siehe Pagination. |
| type | string (≤30) | — | Buchungstyp (z. B. income, expense). |
| status | string (≤30) | — | Status (z. B. confirmed, draft). |
| category | string (≤50) | — | Kategorie (z. B. bürokosten, honorar). |
| from | date (YYYY-MM-DD) | — | Filter: date ≥ from |
| to | date (YYYY-MM-DD) | — | Filter: date ≤ to |
{
"success": true,
"data": [
{
"id": 488,
"date": "2026-04-08",
"amount": 51.00,
"net_amount": 47.02,
"vat_amount": 3.98,
"vat_rate": 8.1,
"vat_category": "normal",
"currency": "CHF",
"method": "card",
"type": "expense",
"status": "confirmed",
"category": "reisekosten",
"remark": "Kundentermin Bern",
"invoice_id": null,
"beleg_nummer": "B-201",
"source": "manual",
"created_at": "2026-04-08T19:02:00.000Z",
"updated_at": "2026-04-08T19:02:00.000Z"
}
],
"pagination": { "page": 1, "limit": 25, "total": 1, "total_pages": 1, "has_next": false, "has_prev": false }
}/v1/standard/bookings/{id}
Einzelne Buchung inklusive MWST-Felder und Kategorie.
Versionierung & Stabilität
Die API-Version ist im Pfad enthalten (/v1/). Breaking Changes erhalten eine neue Major-Version. Wir folgen dieser Policy:
- Additiv = kein Breaking Change: Neue Felder in Responses, neue optionale Query-Parameter, neue Endpoints, neue Enum-Werte.
- Breaking = neue Major-Version: Entfernen/ Umbenennen von Feldern, Pflicht-Parameter ändern, Typ-Wechsel, neue Auth-Anforderungen.
- Deprecation: Deprecated Endpoints liefern zusätzlich einen
Deprecation- undSunset-Header (RFC 9745/8594). Mindestens 6 Monate Vorlauf. - Public Preview: Während
v1als Public Preview läuft, behalten wir uns geringfügige Änderungen an Response-Shapes vor. Breaking Changes werden im Changelog dokumentiert.
Sicherheit & Best Practices
- Key wie Passwort behandeln. Nicht in öffentlichen Repos, Client-seitigem Code, Chat-Nachrichten oder Logs exponieren. Bei Push-to-Git mit versehentlichem Key gilt der Key als kompromittiert — sofort widerrufen.
- Pro Integration ein Key. Kompromittierung eines Keys betrifft nicht Ihre anderen Integrationen und erlaubt gezieltes Widerrufen.
- Sofortiger Widerruf. Verdachtsfall → Key in den Einstellungen widerrufen. Tritt sofort in Kraft (keine Cache-Verzögerung).
- Nur HTTPS. Plain-HTTP-Requests werden abgewiesen. TLS 1.2 Minimum.
- Server-to-Server. Keys gehören in ein Secrets-Management (Vault, 1Password, Doppler, DO App Platform Env-Vars). Niemals im Browser oder Mobile-App.
- Logging. Masken Sie das Bearer-Token in eigenen Logs (nur
pfs_live_***zeigen).
Changelog
- v1.0 — April 2026: Erstveröffentlichung (Public Preview). Lesezugriff auf Rechnungen, Belege und Buchungen für GmbH und Einzelunternehmen. Bearer-Token-Auth, 120 req/min Rate Limit, paginierte JSON-Responses.
Roadmap
- Schreibzugriff (POST/PUT) für Rechnungen und Belege
- Debitoren-/Kreditoren- und Kunden-Endpoints
- Webhooks:
invoice.paid,beleg.created,journal.posted, … - Feingranulare Scopes (
read:invoices,write:belege, …) - OpenAPI 3.1 Spec + interaktiver Playground
- SDK-Wrapper für TypeScript/Node, Python, PHP