Verträge

Endpoints für die Verwaltung von Verträgen — Full-CRUD top-level über `/v3/contracts/{id}` plus parent-scoped Listing/Create unter `/v3/customers/{kid}/contracts`. Sub-Ressourcen (Personen, Tarife) und Annotations hängen über die Vertrag-`id` top-level.

Zuletzt geprüft: 14. Mai 2026

Übersicht

Ein Contract ist ein Vertrag eines Customers — Versicherungspolice, Finanzierungsvertrag, Sparvertrag o. ä. Vertragsdaten umfassen Stammdaten (Vertragsnummer, Gesellschaft, Sparte, Tarif, Beitrag), versicherte Personen, Tarif-Optionen und beliebige angehängte Comments/Documents/Followers/Tags.

Der Contract hat eine global eindeutige id — alle CRUD-Operationen laufen daher top-level über /v3/contracts/{id}, ohne Detour über den Customer:

  1. Top-Level Full-CRUD — /v3/contracts (List, Create) und /v3/contracts/{id} (Read, Update, Delete). Eignet sich für Reports, Dashboards, Provisions-Auswertungen, Workflow-Knoten und überall dort, wo du die Contract-id schon kennst.
  2. Parent-scoped Listing + Create — GET /v3/customers/{kid}/contracts (Verträge eines Kunden) und POST /v3/customers/{kid}/contracts (Vertrag unter einem Kunden anlegen). Path-kid ist Source-of-Truth; Body darf kein kid-Feld tragen (sonst 400 body_kid_forbidden).

Beide Patterns nutzen denselben v3:contracts:*-Scope — der Scope hängt an den Daten, nicht am URL-Pfad.

Scopes

Alle Vertrags-Endpoints — unabhängig vom URL-Pfad — laufen über die Scope-Familie v3:contracts:*:

  • v3:contracts:read — alle GET-Endpoints (Liste, Einzelabruf, Persons-Liste, Tariffs-Liste).
  • v3:contracts:write — POST, PATCH, DELETE auf Vertrag, Persons und Tariffs.
  • Sparten-Stammdaten liegen jetzt unter /v3/system/sparten mit Scope v3:system:read — nicht mehr unter /v3/contracts/sparten.
EndpointScope
GET /v3/contractsv3:contracts:read
POST /v3/contractsv3:contracts:write
GET /v3/contracts/<id>v3:contracts:read
PATCH /v3/contracts/<id>v3:contracts:write
DELETE /v3/contracts/<id>v3:contracts:write
GET /v3/customers/<kid>/contractsv3:contracts:read
POST /v3/customers/<kid>/contractsv3:contracts:write
GET /v3/contracts/<id>/personsv3:contracts:read
POST /v3/contracts/<id>/personsv3:contracts:write
DELETE /v3/contracts/<id>/persons/<personId>v3:contracts:write
GET /v3/contracts/<id>/tariffsv3:contracts:read
POST /v3/contracts/<id>/tariffsv3:contracts:write
DELETE /v3/contracts/<id>/tariffs/<tariffId>v3:contracts:write

Wichtig: v3:customers:read|write reicht NICHT für Vertrags-Zugriff — auch wenn der URL-Pfad /v3/customers/... enthält. Der Scope hängt an den Daten, nicht am Pfad. Wer Customers verwalten will und Verträge lesen will, braucht beide Scopes — getrennt und unabhängig.

Auth-Mechanik und Fehler siehe Authentifizierung. Vollständige Scope-Referenz mit Token-Beispielen: Scopes.

Pagination & Limits

Standard-Pagination (page, limit) wie im Customer-Endpoint. Default-Limit 50, Hard-Cap 5000, Envelope { page, page_size, total, items }. Details und Beispiel-Loop in der Customers-Pagination.

Batch-Create: POST /v3/customers/<kid>/contracts akzeptiert Single-Item oder Array (max. 100 Items). Verhalten identisch zum Customer-POST (per-item Transaktion, gescheiterte Items in failed[], kein „alle-oder-keiner") — siehe Customers > Kunden anlegen.

Schema

json
{
  "id": 90001,
  "kid": 12345,
  "sparte": 164,
  "vertragsnummer": "V-2024-00471",
  // Weitere Vertragsfelder richten sich nach der Sparte und der Organisation.
  // Sub-Ressourcen sind nur mit den jeweiligen with_*-Parametern enthalten.
  "persons": [
    {
      "id": 87001,
      "kid": 12345,
      "vid": 90001,
      "pid": 42,
      "person_art": 2,
      "person_name": "Testperson"
    }
  ],
  "tariffs": [
    {
      "id": 88001,
      "kid": 12345,
      "vid": 90001,
      "name": "PrivatPlus",
      "tarif_id": -1
    }
  ]
}

Im Einzelabruf mit with_schema=true enthält select_fields.sparten alle aktuell verfügbaren Sparten der Organisation; eine ältere, inzwischen deaktivierte Sparte bleibt zusätzlich enthalten, wenn der Vertrag sie verwendet. Mit eingebundenen Personen oder Tarifen liegt die Liste unter select_fields.contracts.sparten. Für eine eigenständige, cachebare Sparten-Liste gibt es GET /v3/system/sparten. Bei paginierten Vertragslisten ohne division werden dagegen nur Sparten der aktuellen Ergebnisseite aufgeführt.

Dynamische bestandsführende Firma (gd_abr_firma)

gd_abr_firma erwartet eine numerische Firmen-ID, keinen Firmennamen:

  • 0 steht für den aktuellen Hauptfirmennamen aus Organisation → Einstellungen → Firma → Firmenname.
  • Positive Werte verweisen auf eine zusätzliche Firma aus den Abrechnungseinstellungen.

Die gültigen Werte stehen bei ?with_schema=true unter select_fields.abrFirma; mit eingebundenen Personen oder Tarifen unter select_fields.contracts.abrFirma. Diese Liste wird dynamisch aus den aktuellen Organisations- und Abrechnungseinstellungen erzeugt; im Formulareditor angelegte statische Optionen sind für dieses Feld nicht maßgeblich.

Die Liste gilt organisationsweit, nicht nur für eine einzelne Sparte. Der Hauptfirmenname wird unter Organisation → Einstellungen → Firma gepflegt; zusätzliche Abrechnungsfirmen werden in den Abrechnungseinstellungen verwaltet. Änderungen der Anzeigenamen verändern bestehende Verträge nicht: Gespeichert bleibt die numerische ID, nur deren aktuelle Bezeichnung wird bei der Anzeige neu aufgelöst.

{
  "fields": {
    "gd_abr_firma": {
      "label": "Bestandsführende Firma",
      "data_type": "number",
      "select_fields_name": "abrFirma"
    }
  },
  "select_fields": {
    "abrFirma": [
      { "value": 0, "display": "Hauptfirma GmbH" },
      { "value": 3, "display": "Weitere Firma GmbH" }
    ]
  }
}

Endpoints

Verträge org-weit abfragen

Liefert eine paginierte Liste aller Verträge der Organisation, quer durch alle Customers. Ideal für Reports und Analytics. Read-only, eigener Scope v3:contracts:read.

Query-Parameter

  • page — Seitenindex (Default 1).
  • limit — Items pro Seite (Default 50, Max 5000).
  • kid — optional: Filter auf einen einzelnen Customer.
  • with_persons — versicherte Personen inline (Default false, opt-in).
  • with_tariffs — Tarif-Optionen inline (Default false, opt-in).
  • with_schema — Schema im Response (Default false, opt-in). Ohne eingebundene Unterressourcen stehen fields und select_fields direkt unter der Antwort. Mit with_persons und/oder with_tariffs liegen sie unter contracts, persons und/oder tariffs – auch wenn ein eingebundenes Array leer ist.
  • division — Sparte-ID aus Sparten-Liste. Filtert sowohl die items[] als auch das (mit with_schema=1 gelieferte) fields/select_fields auf genau diese Sparte. Ideal für Form-Builder, die das Create-Form für eine konkrete Sparte rendern wollen — auch wenn noch kein Vertrag der Sparte existiert (= leere Liste, volles Sparten-Schema).
  • with_archived — auch archivierte Verträge listen (Default false).
  • flat — Sub-Arrays in flache Keys ausrollen (Default false).

Request

bash
curl 'https://www.api.i-planner.app/v3/contracts?page=1&limit=50&with_archived=true' \
  -H "Authorization: Bearer $IPLANNER_API_TOKEN"

Response

json
// OK — Pagination-Envelope + items[]. Jedes Item ist ein vollständiges
// Contract-Objekt. persons[] und tariffs[] erscheinen nur bei den with_*-Parametern.
{
  "page": 1,
  "page_size": 50,
  "total": 4217,
  "items": [
    {
      "id": 90001,
      "kid": 12345,
      "vertragsnummer": "V-2024-00471",
      "sparte": 164
    }
  ]
}

Einzelnen Vertrag abrufen

GET/v3/contracts/<id> Im Playground testen ↗

Liefert einen einzelnen Vertrag über seine globally-unique id (alle Sub-Ressourcen inline). Read-only, Scope v3:contracts:read. Query-Parameter with_persons, with_tariffs, with_schema, flat wie bei der Liste.

Request

bash
curl 'https://www.api.i-planner.app/v3/contracts/90001' \
  -H "Authorization: Bearer $IPLANNER_API_TOKEN"

Response — analog zum Customer-Get: 200 mit vollem Objekt, 400 invalid_id, 404 endpoint_not_found (nicht-numerisch), 404 not_found (Vertrag existiert nicht). Volle Error-Tabelle in Errors.

Vertrag top-level anlegen

Legt einen Vertrag unter einem bestehenden, nicht gelöschten Customer an. Der Customer-kid ist im Body erforderlich; Produkt-, Benutzer-, gelöschte oder unbekannte Kontakte werden nicht als Parent akzeptiert. Alternativ setzt POST /v3/customers/{kid}/contracts den kid über den Pfad.

persons werden weiterhin über ihren eigenen Endpoint angelegt. Tarife können dagegen direkt als tariffs-Array mitgesendet werden. Vertrag und Tarife werden atomar gespeichert: Schlägt ein Tarif fehl, wird auch der Vertrag nicht angelegt.

Request

bash
curl -X POST 'https://www.api.i-planner.app/v3/contracts' \
  -H "Authorization: Bearer $IPLANNER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "kid": 12345,
    "nummer": "H-2026-00012",
    "gesellschaft": "HUK24",
    "sparte": "haftpflicht",
    "beitrag": 7.90,
    "intervall": "monatlich",
    "beginn": "2026-06-01",
    "tariffs": [
      { "name": "PrivatPlus", "tarif_id": -1 },
      { "name": "Zahn Premium", "tarif_id": 42 }
    ]
  }'

Response — Erfolg 201 { success, id, kid, tariffs }; ein fehlender oder ungültiger kid ergibt 400, ein nicht vorhandener, gelöschter oder falsch typisierter Customer 404 customer_not_found. tariffs enthält die tatsächlich angelegten Tarifzeilen einschließlich ihrer neuen IDs.

Vertrag am Kunden anlegen

POST/v3/customers/<kid>/contracts Im Playground testen ↗

Legt einen Vertrag unter einem konkreten Customer an. Der Customer-kid steht im Pfad — kid im Body ist verboten und führt zu 400 body_kid_forbidden.

Diese parent-scoped Variante ist die typische Wahl in UI-Workflows („Vertrag an diesen Kunden anhängen"). Wer die kid bereits im Programm-Kontext hat, kann sie stattdessen an den Top-Level-POST senden.

Request

bash
curl -X POST 'https://www.api.i-planner.app/v3/customers/12345/contracts' \
  -H "Authorization: Bearer $IPLANNER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "nummer": "H-2026-00012",
    "gesellschaft": "HUK24",
    "sparte": "haftpflicht",
    "beitrag": 7.90,
    "intervall": "monatlich",
    "beginn": "2026-06-01",
    "tariffs": [
      { "name": "PrivatPlus", "tarif_id": -1 }
    ]
  }'

Response

json
{
  "success": true,
  "id": 90003,
  "kid": 12345,
  "tariffs": [
    { "id": 88001, "vid": 90003, "kid": 12345, "name": "PrivatPlus", "tarif_id": -1 }
  ]
}

Vertrag aktualisieren

PATCH/v3/contracts/<id> Im Playground testen ↗

Aktualisiert die im Body gesendeten Felder über die globally-unique id — kein Customer-Detour nötig. PATCH-Semantik (Felder nicht im Body bleiben unverändert). Sub-Ressourcen-Arrays werden hier nicht akzeptiert. Tarifänderungen laufen über die Tarif-Unterressource, damit vorhandene Mehrfach-Tarife nicht versehentlich ersetzt werden.

Request

bash
curl -X PATCH 'https://www.api.i-planner.app/v3/contracts/90001' \
  -H "Authorization: Bearer $IPLANNER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "beitrag": 92.50, "status": "aktiv" }'

Response

json
{ "success": true, "updated": 2 }

Vertrag löschen

DELETE/v3/contracts/<id> Im Playground testen ↗

Soft-Delete via del=1 — der Vertrag bleibt audit-fähig in der Datenbank, ist aus Listen-Responses ausgeblendet. Wiederholte DELETEs antworten mit 404 not_found. Top-level über die globally-unique id.

Request

bash
curl -X DELETE 'https://www.api.i-planner.app/v3/contracts/90001' \
  -H "Authorization: Bearer $IPLANNER_API_TOKEN"

Response

json
{ "success": true, "deleted": true, "soft_deleted": true, "id": 90001 }

Helper-Endpoints

Sparten-Liste

GET/v3/system/sparten Im Playground testen ↗

Globale Liste aller Sparten-Werte deiner Organisation. Keine kid-Bindung, keine Pagination — der Endpoint liefert das Tenant-Sparten-Dictionary als kompaktes Array. Cache-friendly auf Client-Seite. Scope: v3:system:read.

Request

bash
curl 'https://www.api.i-planner.app/v3/system/sparten' \
  -H "Authorization: Bearer $IPLANNER_API_TOKEN"

Response

json
{
  "items": [
    { "value": "krankenversicherung", "display": "Krankenversicherung" },
    { "value": "lebensversicherung",  "display": "Lebensversicherung" },
    { "value": "haftpflicht",          "display": "Haftpflicht" },
    { "value": "kfz",                  "display": "KFZ" },
    { "value": "rechtsschutz",         "display": "Rechtsschutz" }
  ]
}

Unterressourcen

Persons

Versicherte Personen oder Vertragsbeteiligte an einem Vertrag (Versicherungsnehmer, Versicherte, Begünstigte).

DB-Tabelle: crm_vertraege_person. Verfügbare Spalten: id, kid, vid, pid, art, person_art, name, person_name, beschreibung, ranking. Unbekannte Body-Felder werden still verworfen.

json
{
  "id": 87001,
  "vid": 90001,                       // Vertrag (aus Pfad)
  "kid": 12345,                       // Parent-Customer (aus Pfad)
  "pid": 67890,                       // optional: kid einer anderen Person, falls verlinkt
  "art": 1,                           // Rollen-Kennung (smallint)
  "person_art": 1,
  "name": "Leier",                    // Freitext-Name
  "person_name": "Helmut Leier",
  "beschreibung": "Hauptversicherter"
}
GET/v3/contracts/<id>/persons Im Playground testen ↗
POST/v3/contracts/<id>/persons Im Playground testen ↗
DELETE/v3/contracts/<id>/persons/<personId> Im Playground testen ↗

Path-Parameter — <id> (Contract-Row-ID, globally unique), <personId> (Person-Row-ID). Query-Parameter (List) — page, limit, with_schema.

Beispiel — Versicherte Person hinzufügen:

bash
curl -X POST 'https://www.api.i-planner.app/v3/contracts/90001/persons' \
  -H "Authorization: Bearer $IPLANNER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "art": 1,
    "person_name": "Anna Leier",
    "beschreibung": "Versicherte Person"
  }'

Tariffs

Tarif-Optionen / Bausteine eines Vertrags (z. B. bei einer Krankenversicherung: stationär, ambulant, Zahn separat).

DB-Tabelle: crm_vertraege_tarife. Schreibbar sind name (Pflicht, maximal 255 Zeichen) und optional tarif_id (-1 oder eine positive Katalog-ID). id, kid, vid und ranking werden vom Server verwaltet; unbekannte Body-Felder werden mit 400 validation_failed abgelehnt.

json
{
  "id": 88001,
  "kid": 12345,                       // Parent-Customer (aus Pfad)
  "vid": 90001,                       // Vertrag (aus Pfad)
  "tarif_id": -1,                     // Referenz auf System-Tarif (-1 = freier Eintrag ohne Katalog-Bezug)
  "name": "Stationär PrivatPlus"
}
GET/v3/contracts/<id>/tariffs Im Playground testen ↗
POST/v3/contracts/<id>/tariffs Im Playground testen ↗
DELETE/v3/contracts/<id>/tariffs/<tariffId> Im Playground testen ↗

Path-Parameter — <id> (Contract-Row-ID, globally unique), <tariffId> (Tarif-Row-ID). Query-Parameter (List) — page, limit, with_schema.

Beispiel — Tarif-Baustein hinzufügen:

bash
curl -X POST 'https://www.api.i-planner.app/v3/contracts/90001/tariffs' \
  -H "Authorization: Bearer $IPLANNER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Zahn Premium",
    "tarif_id": -1
  }'

Anhänge am Vertrag

Comments, Documents, Followers und Tags hängen direkt an der Vertrag-id — top-level, ohne Customer-Detour:

/v3/contracts/<id>/<resource>[/<rid>]

<resource> ist comments, documents, followers oder tags. Felder, Methods und Response-Bodies entsprechen den Customer-Level-Pendants — der einzige Unterschied: kein kid-Body-Feld, kein kid-Query-Param. Beides ergibt sich aus dem Pfad.

bash
curl -X POST 'https://www.api.i-planner.app/v3/contracts/90001/comments' \
  -H "Authorization: Bearer $IPLANNER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Beitragserhöhung zum 1. Juli 2026 angekündigt." }'