Daten anlegen, ändern und löschen
Schreiben geht genauso geradlinig wie Lesen. Zwei Vorkehrungen ersparen dir dabei später viel Ärger — die stehen weiter unten unter „Doppelte Datensätze verhindern“ und „Fremde Änderungen nicht überschreiben“.
1. Anlegen
POST /api/v1/contacts
Content-Type: application/json
{
"lastname": "Müller",
"firstname": "Erika",
"email": "erika.mueller@example.de",
"city": "Musterstadt",
"zip": "12345"
}
Die Antwort hat den Status 201 und enthält den fertigen Datensatz samt seiner neuen Kennung:
{ "data": { "id": 4711, "lastname": "Müller", ... } }
Welche Felder es gibt und welche Pflicht sind, sagt dir vorher /api/v1/contacts/_schema.
2. Ändern
Mit PATCH änderst du nur die Felder, die du mitschickst. Alles andere bleibt, wie es war:
PATCH /api/v1/contacts/4711
{ "city": "Neustadt" }
PUT verhält sich wie PATCH, verlangt aber zusätzlich alle Pflichtfelder. Nicht mitgeschickte Felder bleiben auch hier stehen — die Schnittstelle leert nie stillschweigend etwas. PATCH ist fast immer richtig.
Manche Felder sind nur beim Anlegen schreibbar, danach nicht mehr — etwa der Kontakt einer Rechnung. In _schema tragen sie "immutable": true.
3. Löschen
DELETE /api/v1/contacts/4711
Das verschiebt den Datensatz in den Papierkorb. Er ist damit aus dem Weg, aber nicht weg. Zurückholen:
POST /api/v1/contacts/4711/restore
Endgültig löschen — nur für Administratoren und unwiderruflich:
DELETE /api/v1/contacts/4711?force=1
4. Doppelte Datensätze verhindern
Stell dir vor: Dein Programm schickt „Kontakt anlegen“, und die Verbindung bricht ab. Ist der Kontakt jetzt da oder nicht? Dein Programm weiß es nicht. Schickt es die Anfrage sicherheitshalber noch einmal, hast du den Kontakt womöglich zweimal.
Dagegen gibt es eine einfache Vorkehrung. Schick beim Anlegen einen frei gewählten Wert mit:
POST /api/v1/contacts
Idempotency-Key: bestellung-4711
{ "lastname": "Müller", ... }
Kommt dieselbe Anfrage ein zweites Mal, liefert die Schnittstelle die ursprüngliche Antwort zurück, statt einen zweiten Datensatz anzulegen. Der Wert wird 24 Stunden lang gemerkt.
Als Wert eignet sich alles, was den Vorgang eindeutig kennzeichnet: eine Bestellnummer, eine fortlaufende Nummer aus deinem System, oder einfach eine zufällige Kennung, die dein Programm pro Vorgang einmal erzeugt.
Nimmst du denselben Wert für einen anderen Inhalt, bekommst du einen Fehler 409 — das ist Absicht und schützt vor Verwechslungen.
5. Fremde Änderungen nicht überschreiben
Zweites Szenario: Dein Programm liest einen Kontakt, rechnet ein bisschen, und schreibt eine Minute später zurück. In der Zwischenzeit hat eine Mitarbeiterin denselben Kontakt in der Oberfläche bearbeitet. Ohne Vorkehrung überschreibt dein Programm ihre Arbeit stillschweigend.
Beim Lesen bekommst du eine Stand-Kennung in der Kopfzeile ETag:
GET /api/v1/contacts/4711
ETag: "3f8a91c2b4e07d15"
Schick sie beim Ändern als If-Match mit:
PATCH /api/v1/contacts/4711
If-Match: "3f8a91c2b4e07d15"
{ "city": "Neustadt" }
Hat inzwischen jemand anderes den Datensatz angefasst, kommt Status 412 zurück, und deine Änderung wird nicht ausgeführt. Dein Programm liest dann neu, entscheidet, und schreibt noch einmal.
Dieselbe Kennung spart beim Lesen Übertragung: Schickst du sie als If-None-Match und hat sich nichts geändert, antwortet die Schnittstelle mit 304 und ohne Daten.
6. Prüffehler verstehen
Passt eine Angabe nicht, bekommst du Status 422 und eine Liste der betroffenen Felder:
{
"error": {
"status": 422,
"code": "validation_failed",
"message": "Die übergebenen Daten sind ungültig.",
"details": [
{ "field": "email",
"message": "Keine gültige E-Mail-Adresse." },
{ "field": "zip",
"message": "Zu lang (12 Zeichen, erlaubt sind 10)." },
{ "field": "lastname",
"message": "Nachname ist ein Pflichtfeld." }
]
}
}
Die Meldungen sind so geschrieben, dass du sie einem Benutzer unverändert anzeigen kannst.
Auch ein Tippfehler im Feldnamen wird gemeldet, statt stillschweigend ignoriert zu werden — sonst wunderst du dich, warum eine Angabe nicht ankommt:
{ "field": "nachname",
"message": "Unbekanntes Feld \"nachname\". Die schreibbaren
Felder liefert GET /api/v1/contacts/_schema." }
7. Verweise werden geprüft
Trägst du beim Tier einen Halter ein, der nicht existiert, wird das abgelehnt:
{ "field": "id_contact",
"message": "Kontakt verweist auf \"contacts\" mit der
Kennung 99999 — dieser Datensatz existiert
nicht (oder liegt im Papierkorb)." }
So entstehen über die Schnittstelle keine Datensätze, die ins Leere zeigen.
8. Datumsangaben und Zahlen
Die Schnittstelle nimmt beide Schreibweisen an und gibt einheitlich zurück:
| Du darfst schicken | Du bekommst zurück |
2026-08-17 oder 17.08.2026 | 2026-08-17 |
2026-08-17 14:30 oder 17.08.2026 14:30 | 2026-08-17 14:30:00 |
2026-08-17T12:30:00Z (mit Zeitzone) | 2026-08-17 14:30:00 — umgerechnet in die Zeit deiner Instanz |
12.50 oder "12,50" | 12.5 |
true, 1, "ja" | true |
Uhrzeiten schreibst du als 14:30 oder 14:30:00.
9. Was die Schnittstelle nicht zulässt — und warum
Die Schnittstelle kennt die Regeln der Software. Eine festgeschriebene Rechnung bleibt festgeschrieben, eine stornierte Buchung storniert, eine gebuchte Zahlung gebucht — egal, welchen Schlüssel du benutzt. Solche Datensätze sind gesperrt: Ändern und Löschen beantwortet die Schnittstelle mit Status 409 und dem Code record_locked. Die Meldung sagt dir jeweils, warum und was stattdessen geht.
| Das geht nicht | Stattdessen |
| Festgeschriebene Rechnung ändern oder löschen | POST /api/v1/invoices/{id}/cancel erzeugt eine Korrekturrechnung |
| Rechnungsnummer, Festschreibung oder Summen selbst setzen | POST /api/v1/invoices/{id}/close schreibt den Entwurf fest und vergibt alles |
| Position an eine festgeschriebene Rechnung hängen | Positionen kommen nur an Entwürfe |
| Zahlung ändern oder löschen | POST /api/v1/payments/{id}/cancel bucht sie gegen |
| Zahlung auf einen Rechnungsentwurf | erst festschreiben, dann buchen |
| Stornierte, ausgecheckte oder abgerechnete Buchung oder Termin ändern | Notizfelder bleiben änderbar; alles Weitere in der Oberfläche |
| Kennwort eines Kontakts setzen | gar nicht — Kennwörter vergibt nur die Software |
| E-Mails anlegen, Kassenbuch oder Kundenkonto bebuchen | nur lesen; diese Vorgänge laufen über die Oberfläche |
Welche Bedingung einen Bereich sperrt, steht in GET /api/v1/<bereich>/_schema unter meta.locked_when; die Felder, die trotz Sperre änderbar bleiben, unter meta.writable_when_locked. Zustandsfelder wie finalized, canceled, paid, checkin oder sent sind überall nur lesbar — sie werden von der Software gesetzt, nicht von außen.
10. Eine Rechnung über die Schnittstelle stellen
Der Ablauf ist derselbe wie in der Oberfläche: Entwurf anlegen, Positionen dazu, festschreiben, Zahlung buchen.
POST /api/v1/invoices
{ "id_contact": 4711, "invoicedate": "2026-09-09", "target": "2026-09-23",
"subject": "Rechnung September" }
→ 201, Entwurf mit invoicenumber "ENTWURF"
POST /api/v1/lineitems
{ "id_contact": 4711, "id_invoice": 9338, "servicename": "Einzelstunde",
"servicedate": "2026-09-09", "quantity": 1,
"net_price_item": 50, "end_price_item": 59.50,
"net_price_overall": 50, "end_price_overall": 59.50, "taxrate": 19 }
POST /api/v1/invoices/9338/close
→ 200, Rechnungsnummer vergeben, PDF erzeugt, Rechnung gesperrt
POST /api/v1/payments
{ "id_invoice": 9338, "id_paymenttype": 1, "amount": 59.50,
"paymentdate": "2026-09-10" }
→ 201, Rechnung steht auf „bezahlt“
Die Schnittstelle rechnet dabei den Steueranteil der Zahlung aus, weist Überzahlungen ab und setzt den Rechnungszustand (offen, teilweise bezahlt, bezahlt) — genau wie beim Buchen in der Oberfläche. Eine E-Mail an den Kunden verschickt sie nicht; der Versand bleibt der Oberfläche vorbehalten.
Einen Entwurf, den du nicht mehr brauchst, löschst du mit DELETE /api/v1/invoices/{id}. Seine Positionen bleiben als offene Positionen erhalten. Eine festgeschriebene Rechnung machst du mit POST /api/v1/invoices/{id}/cancel rückgängig: Es entsteht eine Korrekturrechnung mit der Nummer …-S, alle Positionen werden gutgeschrieben und die Zahlungen storniert.