v1 · Public PreviewLesezugriff (GET)JSON · UTF-8

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

  1. API-Key erstellen unter Einstellungen → Integrationen → API-Keys. Der Key wird nur einmal angezeigt.
  2. Den Key im Authorization-Header jedes Requests mitsenden.
  3. 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

PrefixTypBeschreibung
pfs_live_stringProduktiv-Key mit vollem Lesezugriff.
pfs_test_stringReserviert 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-DD für reine Datumsfelder.
  • Geldbeträge: JSON number mit bis zu 2 Nachkommastellen (z. B. 1621.50). Keine Minor-Units. Null = kein Wert, 0 = explizit null.
  • Währung: ISO-4217 (meist CHF, optional EUR, 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.

ParameterTypDefaultBeschreibung
pageinteger11-basierte Seitennummer.
limitinteger (1–100)25Einträ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

CodeTypBeschreibung
200 OKsuccessRequest erfolgreich, Response-Body enthält Daten.
400 Bad RequestclientUngültige Eingabe (z. B. Route-Parameter keine positive Ganzzahl).
401 UnauthorizedclientKein, ungültiger oder widerrufener API-Key. Header prüfen.
403 ForbiddenclientKey existiert, hat aber keinen Zugriff auf diese Ressource (z. B. GmbH-Endpoint mit Einzelfirma-Key).
404 Not FoundclientRessource existiert nicht oder gehört nicht zu Ihrem Account.
429 Too Many RequestsclientRate-Limit überschritten. Retry-After respektieren.
500 Internal Server ErrorserverUnerwarteter 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.

GET

/v1/gmbh/invoices

business_type = gmbh

Paginierte Liste von GmbH-Rechnungen (eingehend und ausgehend).

Query-Parameter

ParameterTypDefaultBeschreibung
pageinteger1Siehe Pagination.
limitinteger (1–100)25Siehe Pagination.
statusenumdraft, sent, paid, partial, overdue, void, cancelled
document_typeenuminvoice, offerte, proforma, credit_note
fromdate (YYYY-MM-DD)Filter: invoice_date ≥ from
todate (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 }
}
GET

/v1/gmbh/invoices/{id}

business_type = gmbh

Einzelne Rechnung. 404 wenn die ID nicht existiert oder nicht zu Ihrem Account gehört. 400 wenn {id} keine positive Ganzzahl ist.

GET

/v1/gmbh/belege

business_type = gmbh

Paginierte 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

ParameterTypDefaultBeschreibung
pageinteger1Siehe Pagination.
limitinteger (1–100)25Siehe Pagination.
kategoriestring (≤50)Belegkategorie (z. B. büromaterial, reisekosten).
fromdate (YYYY-MM-DD)Filter: beleg_datum ≥ from
todate (YYYY-MM-DD)Filter: beleg_datum ≤ to
include_deleted1 | omitBei '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 }
}
GET

/v1/gmbh/belege/{id}

business_type = gmbh

Einzelner Beleg. Keine Original-Datei in der Response.

GET

/v1/gmbh/journal

business_type = gmbh

Paginierte 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

ParameterTypDefaultBeschreibung
pageinteger1Siehe Pagination.
limitinteger (1–100)25Siehe Pagination.
fiscal_year_idintegerAuf ein Geschäftsjahr filtern.
statusenumdraft, posted, voided
sourcestring (≤30)Quelle der Buchung, z. B. invoice, bank_import, manual, payroll.
fromdate (YYYY-MM-DD)Filter: entry_date ≥ from
todate (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 }
}
GET

/v1/gmbh/journal/{id}

business_type = gmbh

Einzelner 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).

GET

/v1/standard/invoices

Paginierte Liste von Rechnungen im Einzelunternehmen-Format.

Query-Parameter

ParameterTypDefaultBeschreibung
pageinteger1Siehe Pagination.
limitinteger (1–100)25Siehe Pagination.
statusstring (≤30)Rechnungsstatus (z. B. draft, sent, paid, overdue).
fromdate (YYYY-MM-DD)Filter: invoice_date ≥ from
todate (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 }
}
GET

/v1/standard/invoices/{id}

Einzelne Rechnung. PDF-Binärdaten werden aus Bandbreiten- und Sicherheitsgründen nie in der Response geliefert.

GET

/v1/standard/belege

Paginierte Liste von Belegen. OCR-Rohtext und S3-Keys werden nicht exponiert.

Query-Parameter

ParameterTypDefaultBeschreibung
pageinteger1Siehe Pagination.
limitinteger (1–100)25Siehe Pagination.
fromdate (YYYY-MM-DD)Filter: beleg_datum ≥ from
todate (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 }
}
GET

/v1/standard/belege/{id}

GET

/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

ParameterTypDefaultBeschreibung
pageinteger1Siehe Pagination.
limitinteger (1–100)25Siehe Pagination.
typestring (≤30)Buchungstyp (z. B. income, expense).
statusstring (≤30)Status (z. B. confirmed, draft).
categorystring (≤50)Kategorie (z. B. bürokosten, honorar).
fromdate (YYYY-MM-DD)Filter: date ≥ from
todate (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 }
}
GET

/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- und Sunset-Header (RFC 9745/8594). Mindestens 6 Monate Vorlauf.
  • Public Preview: Während v1 als 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
Fehler in der Doku entdeckt? Schreiben Sie uns an [email protected].