Zum Inhalt springen
Nexoro

Nexoro REST-API

v1

Alles, was Sie brauchen, um Nexoro an ein anderes System anzubinden — Authentifizierung, Fehlerbehandlung und jeder Endpunkt, mit Beispielen zum Kopieren.

Basis-URLhttps://<mandant>.nexoro.net/rest/v1

Einführung

Die Nexoro REST-API v1 gibt Ihnen denselben Zugriff auf Kontakte, Aufträge, Partner, Produkte und Belege, den auch die Oberfläche nutzt — als JSON, über HTTPS, authentifiziert mit einem Bearer-Token.

Jeder Mandant hat seine eigene Basis-URL. Die Subdomain wählt den Mandanten aus; ein Key, der für einen Mandanten ausgestellt wurde, funktioniert bei keinem anderen.

Basis-URL
http
https://<mandant>.nexoro.net/rest/v1
GrundsatzBeschreibung
JSONAnfragen und Antworten sind application/json; charset=utf-8.
snake_caseAlle Feldnamen der API sind englisch und in snake_case — unabhängig davon, in welcher Sprache die Oberfläche läuft. Die Übersetzung passiert an der API-Grenze, in beide Richtungen.
ISO 8601Datums- und Zeitangaben werden als 2026-05-07T14:30:00Z erwartet. Gängige andere Schreibweisen werden ebenfalls akzeptiert, ISO 8601 ist die verlässliche.
dataErfolgreiche Antworten liegen immer unter data, Listen zusätzlich mit einem pagination-Objekt daneben.
Erfolgs-Envelope
json
{
  "data": { "id": 42, "first_name": "Max", "last_name": "Mustermann" }
}
Erfolgs-Envelope einer Liste
json
{
  "data": [ { "id": 42, "...": "..." } ],
  "pagination": { "limit": 50, "offset": 0, "total": 137 }
}

Authentifizierung

Jeder Request trägt einen API-Key im Authorization-Header. Keys werden im Admin-Center unter Integrationen → REST-API angelegt.

http
Authorization: Bearer nx_<prefix>_<secret>
Ein vollständiger Request
bash
curl -H "Authorization: Bearer nx_xxx_yyy" \
     -H "Content-Type: application/json" \
     "https://your-tenant.nexoro.net/rest/v1/contacts?limit=20"

Der geheime Teil des Keys wird nur einmal bei der Erstellung angezeigt. Bewahren Sie ihn wie ein Passwort auf — serverseitig, nie in Frontend-Code, nie in einem Repository. Ein Key, der unterwegs war, gehört im Admin-Center widerrufen und neu ausgestellt.

Authentifizierungsfehler

StatusWann
401Der Authorization-Header fehlt, ist fehlerhaft aufgebaut, oder der Token gehört zu keinem gültigen Key — etwa weil er widerrufen wurde oder zu einem anderen Mandanten gehört.
403Der Key ist gültig, trägt aber nicht den Scope, den dieser Endpunkt verlangt. Siehe Scopes & Rechte.

Scopes & Rechte

Jeder Key trägt eine Menge von Scopes. Stellen Sie einen Key immer mit der engsten Menge aus, die die Integration wirklich braucht — ein Key, der nur lesen soll, braucht kein orders:write.

ScopeBeschreibung
contacts:read / contacts:writeKontakte lesen bzw. schreiben.
orders:read / orders:writeAufträge lesen bzw. schreiben, einschließlich Status, Kommunikation und angehängter Dateien.
partners:read / partners:writePartner lesen bzw. schreiben.
catalog:readStatus- und Aufgabenkatalog sowie Produkte, Kategorien und Preislisten.
documents:read / documents:writeKaufmännische Belege (Angebot, Rechnung, …) lesen bzw. schreiben, inklusive Bestätigen und PDF-Erzeugung.
documents:sendEinen kaufmännischen Beleg an den Kunden versenden.

documents:send ist bewusst nicht in documents:write enthalten. Es ist die einzige Aktion, die das Haus verlässt und beim Kunden ankommt — ein Key, der Belege erzeugen darf, darf sie deshalb nicht automatisch auch verschicken.

Fehlt der nötige Scope, antwortet die API mit 403 und benennt genau, was gefehlt hat — statt still weniger zu liefern:

json
{
  "type": "about:blank",
  "title": "Forbidden",
  "status": 403,
  "detail": "Dieser API-Key hat den Scope \"documents:write\" nicht.",
  "required_scope": "documents:write",
  "granted_scopes": ["orders:read", "contacts:read"]
}

Im Namen eines Benutzers handeln

Ein Key kann optional an einen CRM-Benutzer gebunden werden. Schreibvorgänge laufen dann mit dessen Rechten und erscheinen im Audit-Log unter seinem Namen (rest:<benutzername>) statt unter der Key-Bezeichnung.

Wird dieser Benutzer später deaktiviert, hört der Key auf zu funktionieren und liefert 403. Das ist beabsichtigt: Der Integrations-Token einer ausgeschiedenen Mitarbeiterin oder eines ausgeschiedenen Mitarbeiters soll deren Konto nicht überleben. Keys ohne gebundenen Benutzer verhalten sich unverändert — dort entscheidet allein der Scope.

Fehler

Fehler kommen als RFC 7807 Problem Details zurück, mit dem Content-Type application/problem+json.

json
{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "Contact 42 not found"
}

Manche Fehler tragen zusätzliche Felder, die genau benennen, was gefehlt hat — etwa required_scope und granted_scopes bei einem 403. Werten Sie immer den Statuscode aus und nutzen Sie detail für Protokolle, nicht für Programmlogik.

StatusWann
200Erfolg, mit Body.
201Angelegt. Der Location-Header zeigt auf die neue Ressource.
204Erfolg, ohne Body.
400Ein Parameter war unbrauchbar, typischerweise ein nicht lesbares Datum.
401Nicht authentifiziert — siehe Authentifizierung.
403Authentifiziert, aber der nötige Scope fehlt — siehe Scopes & Rechte.
404Die Ressource existiert nicht oder ist soft-gelöscht.
405Die HTTP-Methode ist für diesen Pfad nicht vorgesehen.
409Konflikt mit dem aktuellen Zustand — ein bereits versendeter Beleg, ein durch eine Beziehung blockiertes Löschen, oder ein noch laufender Request mit demselben Idempotency-Key.
422Der Body war syntaktisch in Ordnung, hat aber die Validierung nicht bestanden.
500Unerwarteter Serverfehler. Wiederholen Sie den Aufruf mit exponentiellem Backoff.

Anfragevolumen

Drosseln Sie clientseitig auf ein Maß, das zu Ihrem Anwendungsfall passt, statt Endpunkte in einer engen Schleife abzufragen. Für regelmäßige Abgleiche nutzen Sie updated_since und holen Sie nur, was sich seit dem letzten Lauf geändert hat. Wiederholen Sie 5xx-Antworten mit exponentiellem Backoff und einer Obergrenze, nicht sofort.

Paginierung

Alle Listen-Endpunkte paginieren nach demselben Muster: limit und offset hinein, ein pagination-Objekt heraus.

NameTypStandardBeschreibung
limitint50Seitengröße. Auf den meisten Endpunkten bei 200 gedeckelt, beim Kommunikationsverlauf bei 500.
offsetint0Anzahl der zu überspringenden Einträge.
json
"pagination": { "limit": 50, "offset": 0, "total": 137 }

total ist die Gesamtzahl der Treffer für die angefragten Filter, nicht die Länge der aktuellen Seite. Solange offset + limit kleiner als total ist, gibt es eine weitere Seite.

Idempotenz

Schicken Sie einen Idempotency-Key-Header auf POST, PUT oder PATCH, um eine Wiederholung ungefährlich zu machen.

Am wichtigsten ist das überall dort, wo ein Nummernkreis verbraucht wird: Ohne den Header legt ein wiederholtes POST /v1/commercial_documents einen zweiten Beleg an und verbrennt eine Nummer.

FallBeschreibung
Erster AufrufWird normal verarbeitet. Die Antwort wird 30 Tage lang gespeichert.
Wiederholung, gleicher BodyDie gespeicherte Antwort kommt wortgleich zurück, zusammen mit dem Header Idempotent-Replay: true. Nichts wird ein zweites Mal angelegt.
Wiederholung, während der erste Aufruf noch läuft409 Request In Progress. Versuchen Sie es später erneut — ein erster Aufruf, der nie fertig wurde, wird nach fünf Minuten freigegeben.
Gleicher Key, anderer Body422 Idempotency-Key Mismatch. Keys gelten je API-Key, zwei Integrationen können also nicht kollidieren.
Serverfehler (5xx)Werden nicht gespeichert. Eine Wiederholung nach einem Absturz versucht es also wirklich noch einmal.

Keys sind freie Zeichenketten bis 255 Zeichen. Üblich ist eine UUID je logischem Vorgang.

bash
curl -X POST \
  -H "Authorization: Bearer nx_xxx_yyy" \
  -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
  -H "Content-Type: application/json" \
  -d '{"...":"..."}' \
  "https://your-tenant.nexoro.net/rest/v1/orders"

Kontakte

Der Kontakt ist der Stammdatensatz einer Person oder eines Unternehmens — langlebig und die Quelle der Wahrheit dafür, wer jemand ist.

GET/v1/contactscontacts:read

Kontakte auflisten

Liefert eine paginierte Liste von Kontakten.

Query-Parameter

NameTypStandardBeschreibung
limitint50Seitengröße, maximal 200.
offsetint0Versatz für die Paginierung.
emailstring—Filter auf exakte Übereinstimmung.
companystring—Filter auf exakte Übereinstimmung des Firmennamens.
updated_sinceISO date—Nur Kontakte, deren updated_at größer oder gleich diesem Zeitpunkt ist.
include_inactive1 / 00Liefert zusätzlich soft-gelöschte Kontakte (active = 0).
rolestring—Nur Kontakte mit dieser Rolle, nach Name und ohne Rücksicht auf Groß-/Kleinschreibung. Siehe Kontaktrollen.
Antwort — 200
json
{
  "data": [
    {
      "id": 42,
      "first_name": "Max",
      "last_name": "Mustermann",
      "email": "max@example.com",
      "phone": "+43 1 12345678",
      "mobile": null,
      "company": "Acme GmbH",
      "salutation": "Herr",
      "title": null,
      "position": "Geschäftsführung",
      "street": "Hauptstraße 1",
      "postal_code": "1010",
      "city": "Wien",
      "country": "AT",
      "website": "https://acme.example",
      "vat_id": "ATU12345678",
      "note": null,
      "partner_id": 79,
      "active": 1,
      "roles": [
        { "id": 3, "name": "Bauleiter", "color": "" }
      ],
      "created_at": "2026-05-01 10:00:00",
      "updated_at": "2026-05-07 14:00:00"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "total": 137 }
}
Beispiel
bash
curl -H "Authorization: Bearer nx_xxx_yyy" \
  "https://your-tenant.nexoro.net/rest/v1/contacts?limit=20&company=Acme%20GmbH"
StatusWann
400updated_since konnte nicht als Datum gelesen werden.

Kontaktrollen

Ein Kontakt kann beliebig viele Rollen tragen — die Funktion, die diese Person in Ihren Prozessen hat: Bauleiter, Polier, Statiker, Einkauf. Das ist etwas anderes als position, die einzelne freie Berufsbezeichnung innerhalb des eigenen Unternehmens.

  • Rollen hängen am Kontakt, nicht am Auftrag. Ein Kontakt behält seine Rollen über jeden Auftrag hinweg, in dem er vorkommt.
  • Der Katalog ist mandantenweit und füllt sich selbst. Ein Rollenname, den es noch nicht gibt, wird beim Schreiben angelegt — es gibt keinen eigenen Aufruf zum Anlegen einer Rolle und damit auch keine Reihenfolge, die Sie einhalten müssten.

Rollen werden auf jeder Kontakt-Darstellung mitgeliefert (Liste, Einzelabruf, Anlegen, Ändern) und ebenso auf den contacts, die in einem Auftrag eingebettet sind — immer in derselben Form:

json
"roles": [ { "id": 3, "name": "Bauleiter", "color": "" } ]

Zum Schreiben schicken Sie roles an POST oder PATCH /v1/contacts. Die Liste ersetzt die Rollen des Kontakts vollständig:

json
{ "roles": ["Bauleiter", "Polier"] }

Objekte werden ebenfalls angenommen, Sie können also zurückschicken, was Sie gelesen haben: {"roles": [{"name": "Bauleiter"}]}. Ein leeres Array entfernt alle Rollen, ein weggelassenes Feld lässt sie unberührt. Namen sind auf 80 Zeichen begrenzt und werden ohne Rücksicht auf Groß-/Kleinschreibung zugeordnet — polier und Polier sind dieselbe Rolle.

GET/v1/contact_rolescontacts:read

Rollenkatalog abrufen

Jeder Rollenname, den dieser Mandant verwendet — damit ein Client sie nicht raten muss. Inaktive Rollen werden nicht gelistet.

Antwort — 200
json
{
  "data": [
    { "id": 1, "name": "Auftraggeber", "color": "", "aktiv": true },
    { "id": 3, "name": "Bauleiter",    "color": "", "aktiv": true }
  ]
}
Beispiel
bash
curl -H "Authorization: Bearer nx_xxx_yyy" \
  "https://your-tenant.nexoro.net/rest/v1/contact_roles"
GET/v1/contacts/{id}contacts:read

Einen Kontakt abrufen

Liefert den Kontakt samt seinen endpoints — zusätzlichen E-Mail-Adressen, Telefonnummern und weiteren Kanälen.

Antwort — 200
json
{
  "data": {
    "id": 42,
    "first_name": "Max",
    "last_name": "Mustermann",
    "email": "max@example.com",
    "...": "...",
    "endpoints": {
      "email": [
        { "id": 9, "subtype": "primary", "value": "max@example.com" }
      ],
      "phone": [
        { "id": 10, "subtype": null, "value": "+43 1 12345678" }
      ],
      "mobil": [
        { "id": 11, "subtype": null, "value": "+43 660 1234567" }
      ]
    }
  }
}

endpoints ist ein Objekt, dessen Schlüssel der Kanaltyp ist (email, phone, mobil, xmpp, …). Jeder Wert ist ein Array, weil ein Kontakt mehrere Endpunkte desselben Typs haben kann. Typen ohne Einträge fehlen im Objekt ganz.

StatusWann
404Der Kontakt existiert nicht.
POST/v1/contactscontacts:write

Kontakt anlegen

Mindestens eines von email, phone oder mobile muss vorhanden sein.

Schreibbare Felder

  • first_name
  • last_name
  • email
  • phone
  • mobile
  • company
  • salutation
  • title
  • position
  • street
  • door
  • postal_code
  • city
  • country
  • note
  • website
  • vat_id
  • birthday
  • group_id
  • lat
  • lng
  • roles
  • endpoints

source ist nur lesbar. Das Feld ist die Herkunft des Kontakts aus einer Synchronisation (graph, google, reonic, imap) und wird intern gesetzt — es ist kein Marketing-Attributionsfeld. Für die Herkunft eines Auftrags oder Leads nutzen Sie source auf POST/PATCH /v1/orders.

email, phone und mobile am Kontakt sind die primären Werte, bequem denormalisiert. Der vollständige Satz an Kanälen — mehrere E-Mail-Adressen, Telefonnummern, Mobilnummern — liegt im Objekt endpoints, nach Kanaltyp gruppiert.

Optional nehmen Sie endpoints als Array von { "type": "email|phone|mobil|...", "subtype": "primary|...", "value": "..." } mit. Die Werte für type sind freie Domänenbegriffe; Altbestände verwenden mobil für Mobilnummern — schreiben Sie neue Einträge so, wie es zu Ihren vorhandenen Datensätzen passt.

Anfrage
json
{
  "first_name": "Max",
  "last_name": "Mustermann",
  "email": "max@example.com",
  "company": "Acme GmbH",
  "endpoints": [
    { "type": "phone", "value": "+43 1 12345678" }
  ]
}

Antwort — 201 mit dem vollständigen angelegten Kontakt inklusive endpoints, dazu der Header Location: /rest/v1/contacts/{id}.

StatusWann
422Keines von email, phone oder mobile war vorhanden, oder der Body enthielt kein schreibbares Feld.
PATCH/v1/contacts/{id}contacts:write

Kontakt ändern

Teilweise Aktualisierung: Nur Felder, die im Body vorkommen, werden geändert. updated_at rückt automatisch nach.

Anfrage
json
{ "company": "Acme Holding GmbH", "city": "Wien" }

Antwort — 200 mit dem vollständigen aktualisierten Kontakt inklusive endpoints.

StatusWann
404Der Kontakt existiert nicht oder ist soft-gelöscht (active = 0).
422Kein schreibbares Feld im Body. roles zählt als eines — ein Patch, der nur Rollen setzt, wird angenommen.
DELETE/v1/contacts/{id}contacts:write

Kontakt löschen

Standardverhalten ist ein Soft-Delete: active wird auf 0 gesetzt. Der Datensatz bleibt erhalten und verschwindet aus der Standard-Liste — mit ?include_inactive=1 ist er weiterhin sichtbar.

Mit ?hard=1 (oder ?hard=true) wird stattdessen endgültig gelöscht: der Kontakt, seine Endpunkte, seine Verknüpfungen zu Aufträgen, seine Änderungshistorie und seine Rollenzuweisungen. Das ist nicht rückgängig zu machen. Der Rollen-Katalog selbst bleibt unberührt — eine Rolle überlebt den letzten Kontakt, der sie getragen hat.

Antwort — 204, ohne Body.

StatusWann
404Der Kontakt existiert nicht.
409Das endgültige Löschen ist durch eine bestehende Beziehung in der Datenbank blockiert.

Aufträge

Ein Auftrag ist die zentrale Arbeitseinheit im CRM. Er kann einen Datenrettungsfall, ein Photovoltaik-Projekt oder ein Bauvorhaben abbilden — dieselbe Struktur, je Mandant anders ausgeprägt.

Kontakt und Kunde sind zweierlei

Das CRM führt zwei parallele Datensätze für Personen und Unternehmen, und die API zeigt beide. Sie haben unterschiedliche Aufgaben.

FeldBeschreibung
contactsDer Stammdatensatz. Langlebig, die Quelle der Wahrheit dafür, wer jemand ist. Verwaltet über /v1/contacts.
customersEine Rechnungs-Momentaufnahme des Kontakts zum Zeitpunkt des Auftrags. Bewusst vom Stammdatensatz entkoppelt, damit alte Rechnungen stabil bleiben, wenn sich Kontaktdaten später ändern. Die Auftrags-Endpunkte pflegen diese Momentaufnahmen automatisch; einen eigenen CRUD-Bereich dafür gibt es absichtlich nicht.

Jeder Auftrag hat seine eigene Momentaufnahme. Einen Auftrag anzulegen erzeugt eine frische; den customer-Block eines Auftrags zu ändern ändert nur dessen Momentaufnahme und niemals den Stammkontakt.

Das Antwortmodell

Die meisten Felder eines Auftrags sind mandanten- und branchenspezifisch. Die API verbirgt sie hinter einem stabilen, allgemeinen Rahmen:

FeldBeschreibung
id, order_number, title, active, created_at, updated_at, received_atKennungen und Zeitstempel.
current_status{ status_id, status_name, at, user } — der jüngste aktive Eintrag der Statushistorie.
customerDie Rechnungs-Momentaufnahme des Auftrags: Name, Firma, Adresse, UID, Rabatt, Sprache. Je Auftrag über den customer-Block eines PATCH änderbar.
siteDie Durchführungsadresse — der Ort der Leistung, getrennt von der Rechnungsadresse in customer.
contactsAlle Ansprechpartner, die am Kunden dieses Auftrags hängen.
partner_id, source, responsible, websiteSkalare Felder, die über alle Mandanten hinweg dieselbe Bedeutung haben.
current_status_id, current_status_at, current_task_id, current_task_atDirekt lesbare Zeiger auf den aktuellen Status und die aktuelle Aufgabe, von der API konsistent gehalten.
sectionsDie mandantenspezifischen Zusatzfelder, mit den Werten dieses Auftrags überlagert.

contacts wird so aufgebaut: Ausgangspunkt sind alle Ansprechpartner, die am Kunden des Auftrags hängen — jeder mit seinem link_type, häufig "primary". Ist am Auftrag ein primärer Ansprechpartner gesetzt, der noch nicht dabei ist, wird er ergänzt. Genau dieser wird mit is_primary: true markiert und zuerst sortiert; die übrigen folgen nach aufsteigender id. Jeder Eintrag trägt seine roles, die hier nur lesbar sind — Rollen gehören zur Person und nicht zu diesem einen Auftrag, geschrieben werden sie über /v1/contacts. Hat der Auftrag weder Kunde noch primären Ansprechpartner, ist contacts leer.

Der site-Block — die Durchführungsadresse

Wo die Arbeit stattfindet, im Gegensatz dazu, wohin die Rechnung geht. Die Adresse erscheint auf Angeboten, Auftragsbestätigungen, Rechnungen, Gutschriften, Lieferscheinen und Serviceberichten. Der Block ist an jedem Auftrag immer vorhanden.

json
"site": {
  "same_as_billing": false,
  "company": "Baustelle Nord",
  "contact_person": "Herr Huber",
  "street": "Baugasse 1",
  "postal_code": "5020",
  "city": "Salzburg",
  "email": null,
  "phone": "+43 662 1234"
}

same_as_billing: true bedeutet, dass der Auftrag keine eigene Durchführungsadresse hat und die Adresse aus customer gilt. Das ist der Ausgangszustand jedes Auftrags.

Zum Schreiben schicken Sie ein site-Objekt an POST oder PATCH /v1/orders. Zwei Regeln sind dabei wichtig:

  • Ein beliebiges nicht leeres Adressfeld zu schreiben setzt same_as_billing automatisch auf false. Ohne das würde die Adresse gespeichert und anschließend überall ignoriert — CRM und Belegerzeugung überspringen die Durchführungsadresse, solange der Auftrag nicht ausdrücklich als solcher markiert ist. Sie müssen das Kennzeichen also nicht selbst mitschicken.
  • {"site": {"same_as_billing": true}} löscht die Adresse wieder und stellt den Auftrag auf die Rechnungsadresse zurück.
json
{ "site": { "street": "Baugasse 1", "postal_code": "5020", "city": "Salzburg" } }

Weggelassene Felder bleiben unberührt; ein leerer String löscht ein einzelnes Feld. Unbekannte Schlüssel innerhalb von site werden mit 422 abgewiesen, statt stillschweigend zu verschwinden.

Die Adresse erscheint auf erzeugten PDFs nur, wenn beim Mandanten in den Beleg-Einstellungen Durchführungsadresse anzeigen aktiviert ist. Das ist eine Einstellung im CRM, nicht etwas, das die API steuert.

Mandantenspezifische Felder

Die je Mandant definierten Felder liegen in sections[]. Jede Section hat eine geordnete Liste von Einträgen der Form { id, name, type, value, options? }. Als Typen gibt es text, textarea, select, checkbox und termin. value ist der Wert dieses Auftrags und null, solange nichts geschrieben wurde; options gibt es nur bei select.

Zum Schreiben schicken Sie ein custom_fields-Objekt an POST oder PATCH. Die Schlüssel sind Item-IDs, die Sie unter sections[*].items[*].id finden. null löscht ein Feld, unbekannte IDs werden ignoriert.

json
{ "custom_fields": { "66": "ABC-123", "55": "30 Tage" } }

Wiederholbare Sections

Eine Section kann im Admin-Center als wiederholbar markiert werden. Sie hält dann je Auftrag eine Liste von Einträgen statt eines einzelnen Wertesatzes — drei Stapler, zehn Notebooks, mehrere Zählpunkte.

json
{
  "id": 12, "heading": "Hardware", "is_repeatable": true,
  "entry_label": { "singular": "Gerät", "plural": "Geräte" },
  "entries": [
    { "id": 7, "sort_order": 0, "values": { "34": "ThinkPad T14", "35": "SN-001" } },
    { "id": 8, "sort_order": 1, "values": { "34": "MacBook Air",  "35": "SN-002" } }
  ],
  "items": [ { "id": 34, "name": "Modell", "type": "text", "value": "ThinkPad T14" } ]
}

items[].value trägt den Wert des ältesten Eintrags, damit ein Client, der nur die flache Form liest, weiter funktioniert. Nicht wiederholbare Sections bleiben unverändert und haben kein entries.

Schreiben mit custom_field_entries
json
{ "custom_field_entries": [
    { "section_id": 12, "entries": [
        { "values": { "34": "ThinkPad T14", "35": "SN-001" } },
        { "values": { "34": "MacBook Air",  "35": "SN-002" } } ] } ] }

Je Section wird ersetzt, nicht ergänzt: Die Liste, die Sie schicken, wird zu den Einträgen dieser Section für diesen Auftrag, in genau dieser Reihenfolge. Die Eintrags-IDs vergibt der Server, Sie schicken sie nicht mit. custom_fields kann eine wiederholbare Section nicht ansprechen — eine Item-ID, die zu einer gehört, liefert 422 und benennt die betroffenen IDs.

422 kommt auch bei einer unbekannten section_id, einer nicht wiederholbaren Section, einer Item-ID, die nicht zur genannten Section gehört, einem fehlerhaften Eintrag oder mehr als 200 Einträgen in einer Section. Die Prüfung läuft vollständig, bevor irgendetwas ersetzt wird — ein abgewiesener Request lässt die bestehenden Einträge also unangetastet.

Das Statusmodell

Jeder Auftrag hat einen aktuellen Status. Statuswechsel werden nur angehängt, nie überschrieben — die Historie bleibt vollständig erhalten, und die direkt lesbaren Zeiger am Auftrag hält die API automatisch synchron.

Ein Statuswechsel schreibt zugleich den passenden Aufgabeneintrag, damit die Aufgaben-Pipeline des CRM konsistent bleibt. Das entspricht genau dem, was die Statusschaltfläche in der Oberfläche tut.

GET/v1/ordersorders:read

Aufträge auflisten

Query-Parameter

NameTypStandardBeschreibung
limitint50Seitengröße, maximal 200.
offsetint0Versatz für die Paginierung.
customer_idint—Filter auf exakte Übereinstimmung.
customer_emailstring—Exakte Übereinstimmung auf der E-Mail-Adresse der Rechnungs-Momentaufnahme.
contact_emailstring—Exakte Übereinstimmung auf der E-Mail-Adresse eines verknüpften Kontakts. Damit finden Sie alle Aufträge einer Person über die einzelnen Momentaufnahmen hinweg.
partner_idint—Filter auf exakte Übereinstimmung.
order_numberstring—Filter auf exakte Übereinstimmung.
status_idint—Nur Aufträge, deren aktueller Status dieser ID entspricht.
updated_sinceISO date—Nur Aufträge, deren updated_at größer oder gleich diesem Zeitpunkt ist.
include_inactive1 / 00Liefert zusätzlich soft-gelöschte Aufträge.
includecsv—Kommagetrennte Zusätze, die eingebettet werden sollen. Derzeit: sections.

Die Liste liefert den sections-Block je Auftrag standardmäßig nicht mit — das wären N zusätzliche Zeilen pro Auftrag. Fordern Sie ihn mit ?include=sections an, wenn Sie ihn brauchen.

Antwort — 200
json
{
  "data": [
    {
      "id": 7421,
      "order_number": "EXT-7421",
      "title": "Neubau Halle 3",
      "active": 1,
      "partner_id": null,
      "source": "API",
      "responsible": "rest-api",
      "website": null,
      "received_at": "2026-05-07 14:00:00",
      "created_at": "2026-05-07 14:00:00",
      "updated_at": "2026-05-07 14:00:00",
      "current_status_id": 1,
      "current_status_at": "2026-05-07 14:00:00",
      "current_task_id": 1,
      "current_task_at": "2026-05-07 14:00:00",
      "current_status": {
        "status_id": 1,
        "status_name": "Eingegangen",
        "at": "2026-05-07 14:00:00",
        "user": "rest-api"
      },
      "customer": {
        "id": 4612, "customer_number": "EXT-4612",
        "first_name": null, "last_name": null, "company": "Acme GmbH",
        "email": "office@acme.example", "phone": "+43 1 12345678",
        "street": "Hauptstraße 1", "postal_code": "1010",
        "city": "Wien", "country": "AT",
        "vat_id": "ATU12345678", "vat_exempt": 0, "language": "de",
        "partner_id": 79,
        "created_at": "2026-05-07 14:00:00",
        "updated_at": "2026-05-07 14:00:00"
      },
      "contacts": [
        {
          "id": 11034, "company": null,
          "first_name": "Anna", "last_name": "Müller",
          "email": "anna.mueller@acme.example",
          "position": "Geschäftsführung", "partner_id": 79,
          "is_primary": true, "link_type": "primary"
        },
        {
          "id": 11202, "company": null,
          "first_name": "Bernd", "last_name": "Schmid",
          "email": "b.schmid@acme.example",
          "position": "Bauleitung", "partner_id": 79,
          "is_primary": false, "link_type": "primary"
        }
      ]
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "total": 1 }
}
GET/v1/orders/{id}orders:read

Einen Auftrag abrufen

Enthält immer das sections-Array des Mandanten, mit den Werten dieses Auftrags überlagert.

Antwort — 200
json
{
  "data": {
    "id": 7421,
    "order_number": "EXT-7421",
    "title": "Neubau Halle 3",
    "...": "...",
    "current_status": {
      "status_id": 1, "status_name": "Eingegangen",
      "at": "...", "user": "rest-api"
    },
    "customer": { "id": 4012, "company": "Acme GmbH", "...": "..." },
    "contacts": [
      { "id": 11034, "first_name": "Anna", "last_name": "Müller",
        "is_primary": true,  "link_type": "primary", "...": "..." },
      { "id": 11202, "first_name": "Bernd", "last_name": "Schmid",
        "is_primary": false, "link_type": "primary", "...": "..." }
    ],
    "sections": [
      {
        "id": 8,
        "heading": "Lead Details",
        "order": 1,
        "items": [
          { "id": 66, "name": "Lead Nummer bei Leadportal",
            "type": "text", "order": 1, "value": "L-9912" },
          { "id": 55, "name": "Alter des Leads im Portal",
            "type": "select", "order": 2, "value": "30 Tage",
            "options": ["1 Tag", "7 Tage", "30 Tage"] },
          { "id": 58, "name": "Informationen Portal",
            "type": "textarea", "order": 6, "value": null }
        ]
      }
    ]
  }
}
POST/v1/ordersorders:write

Auftrag anlegen

Beim Anlegen läuft immer derselbe Ablauf:

  • Kontakt auflösen — finden oder anlegen. Ist primary_contact_id angegeben, wird sie unverändert übernommen. Sonst wird ein aktiver Kontakt mit derselben E-Mail-Adresse wie customer.email wiederverwendet. Sonst wird ein neuer Kontakt aus den Kundendaten angelegt, sofern mindestens eines von E-Mail, Telefon oder Mobil vorliegt. Andernfalls wird kein Kontakt verknüpft — der seltene Fall eines reinen Adressblocks.
  • Rechnungs-Momentaufnahme erzeugen. Zuerst werden Name, Firma, Adresse, E-Mail, Telefon und Sprache vom aufgelösten Kontakt kopiert, danach überschreibt ein mitgeschickter customer-Block diese Kopie.
  • Kontakt und Kunde verknüpfen und den aufgelösten Kontakt als primären Ansprechpartner des Auftrags setzen.
  • Den Auftrag anlegen und auf die Momentaufnahme zeigen lassen. Ohne mitgeschickte Nummer wird order_number auf EXT-<id> gesetzt.
  • Den ersten Status- und Aufgabeneintrag schreiben.
  • Mitgeschickte custom_fields persistieren.

Sie müssen mindestens eines von primary_contact_id oder customer mitschicken — sonst hätte die Momentaufnahme nichts festzuhalten, und der Aufruf wird mit 422 abgewiesen.

Schreibbare Felder am Auftrag

  • title
  • order_number
  • partner_id
  • primary_contact_id
  • inquiry_contact_id
  • company
  • website
  • source
  • responsible
  • received_at

customer_id ist nicht direkt schreibbar — der Server setzt es, wenn er die Momentaufnahme anlegt.

Schreibbare Felder im customer-Block

  • first_name
  • last_name
  • email
  • phone
  • mobile
  • company
  • salutation
  • title
  • street
  • door
  • postal_code
  • city
  • country
  • vat_id
  • website
  • discount
  • discount_code
  • send_sms
  • send_email
  • vat_exempt
  • language
  • note
  • partner_id

Schreibbare Felder im site-Block

  • company
  • contact_person
  • street
  • postal_code
  • city
  • email
  • phone
  • same_as_billing
Anfrage — Minimum, Momentaufnahme aus bestehendem Kontakt
json
{ "primary_contact_id": 11034 }
Anfrage — Minimum, Kundendaten inline
json
{
  "customer": {
    "first_name": "Max",
    "last_name": "Mustermann",
    "email": "max@example.com"
  }
}
Anfrage — vollständig
json
{
  "title": "Neubau Halle 3",
  "primary_contact_id": 11034,
  "customer": {
    "company": "Acme Holding GmbH",
    "vat_id": "ATU12345678",
    "street": "Hauptstraße 1",
    "postal_code": "1010",
    "city": "Wien",
    "vat_exempt": 1,
    "language": "de"
  },
  "site": {
    "company": "Baustelle Halle 3",
    "contact_person": "Herr Huber",
    "street": "Baugasse 1",
    "postal_code": "5020",
    "city": "Salzburg",
    "phone": "+43 662 1234"
  },
  "source": "API",
  "custom_fields": {
    "66": "L-9912",
    "55": "30 Tage"
  }
}

Antwort — 201 mit dem angelegten Auftrag samt vollständigem customer-Block, contacts, current_status und sections, dazu der Header Location: /rest/v1/orders/{id}.

PATCH/v1/orders/{id}orders:write

Auftrag ändern

Teilweise Aktualisierung, in beliebiger Kombination: Kernfelder wie title, responsible oder source am Auftrag selbst, custom_fields für mandantenspezifische Werte, customer für die Rechnungs-Momentaufnahme dieses Auftrags und site für die Durchführungsadresse. Jede Änderung wird in der Änderungshistorie des Auftrags protokolliert.

json
{
  "title": "Halle 3 — Erweiterung",
  "custom_fields": { "66": "L-9912-B" },
  "customer": {
    "company": "Acme Holding GmbH (umbenannt)",
    "vat_exempt": 1
  },
  "site": { "street": "Baugasse 1a" }
}
  • Ein mandantenspezifisches Feld leeren: schicken Sie null als Wert — der zugehörige Eintrag wird entfernt.
  • Rechnungsdaten ändern: nur die Felder, die Sie in customer mitschicken, werden geschrieben, der Rest der Momentaufnahme bleibt unberührt. Der verknüpfte Stammkontakt ändert sich dabei in keiner Weise — genau dafür gibt es die getrennte Momentaufnahme.
StatusWann
404Der Auftrag existiert nicht oder ist soft-gelöscht.
422custom_fields oder customer ist kein Objekt, custom_field_entries ist kein Array oder wurde abgewiesen, oder es gab nichts zu ändern. Ein Body, der nur custom_field_entries enthält, gilt als Änderung.
DELETE/v1/orders/{id}orders:write

Auftrag löschen

Standardverhalten ist ein Soft-Delete: Der Auftrag wird inaktiv gesetzt, Status- und Aufgabenhistorie bleiben erhalten.

Mit ?hard=1 (oder ?hard=true) wird der Auftrag endgültig entfernt, samt Status-, Aufgaben- und Änderungshistorie; die Werte der mandantenspezifischen Felder fallen mit weg. Das ist nicht rückgängig zu machen.

StatusWann
404Der Auftrag existiert nicht.
409Das endgültige Löschen ist durch eine bestehende Beziehung in der Datenbank blockiert.
POST/v1/orders/{id}/statusorders:write

Status wechseln

Hängt einen neuen Eintrag an die Statushistorie, schreibt den dazu passenden Aufgabeneintrag und aktualisiert die direkt lesbaren Zeiger am Auftrag.

Anfrage
json
{ "status_id": 4 }

Antwort — 200 mit dem Auftrag und seinem neuen current_status.

Ist im Aufgabenkatalog keine Aufgabe auf diesen Status abgebildet, wird der Aufgabeneintrag trotzdem geschrieben — mit der Aufgabe 0, genau wie das CRM sich in diesem Fall selbst verhält.

StatusWann
404Der Auftrag existiert nicht.
422status_id fehlt oder ist unbekannt.
GET/v1/orders/{id}/status_logorders:read

Vollständige Statushistorie

Liefert die letzten 200 Einträge, neueste zuerst, jeweils mit dem Namen des Status.

Antwort — 200
json
{
  "data": [
    { "id": 99012, "status_id": 4, "status_name": "Analyse",
      "workflow": 1, "user": "rest-api",
      "at": "2026-05-07 15:30:00", "inactive": 0 },
    { "id": 99011, "status_id": 1, "status_name": "Eingegangen",
      "workflow": 1, "user": "rest-api",
      "at": "2026-05-07 14:00:00", "inactive": 0 }
  ]
}
GET/v1/orders/{id}/communicationsorders:read

Kommunikationsverlauf

Liefert jede Kommunikation zu diesem Auftrag über alle Kanäle hinweg — E-Mails, WhatsApp-Nachrichten sowie ein- und ausgehende Anrufe — zu einem einzigen chronologischen Verlauf zusammengeführt, neueste zuerst.

Jeder Eintrag wird in denselben Rahmen normalisiert, unabhängig davon, über welchen Kanal er kam:

json
{
  "type": "email | whatsapp | call",
  "source": "<Quellbezeichner des Kanals>",
  "direction": "inbound | outbound",
  "at": "2026-05-07 14:30:00",
  "id": 12345,
  "subject": "...",
  "preview": "Kurzer Textauszug, HTML entfernt, max. ~240 Zeichen",
  "from": "Name <adresse>",
  "to":   "Name <adresse>",
  "user": "CRM-Benutzer, der es bearbeitet hat, falls vorhanden",
  "meta": { "...kanalspezifische Zusätze..." }
}

Die Paginierung greift nach dem Zusammenführen aller Kanäle — limit und offset arbeiten also auf dem vereinigten Verlauf, nicht auf einem einzelnen Kanal.

Query-Parameter

NameTypStandardBeschreibung
typecsvalleKanäle einschränken. Beliebige Teilmenge von email, whatsapp, call — etwa type=email,call.
directionstringbeideinbound oder outbound.
sinceISO date—Nur Einträge ab diesem Zeitpunkt.
limitint50Seitengröße, maximal 500.
offsetint0Versatz innerhalb des vereinigten Verlaufs.

meta je Kanal

KanalBeschreibung
emailbody, html, folder, kontoid, contact_id, unread, answered, forwarded, read_date sowie attachments, wenn die Nachricht über das CRM verschickt wurde.
whatsappmessage, whatsapp_number_id, endpoint_id sowie unread bei eingehenden Nachrichten.
callduration_seconds, ring_seconds, caller_type, answer_time, hangup_time, notes, contact_id, partner_id, herkunft sowie caller_email bzw. callee_email.
StatusWann
404Der Auftrag existiert nicht.
400since konnte nicht als Datum gelesen werden.
GET/v1/orders/{id}/documentsorders:read

Dateien des Auftrags auflisten

Nicht zu verwechseln mit den kaufmännischen Belegen. Hier geht es um die Dateien im Ordner des Auftrags; die Belege selbst, mit Positionen und Summen, liegen unter /v1/commercial_documents.

Die Dateien eines Auftrags liegen in einem Verzeichnisbaum, dessen Wurzel je Auftrag aufgelöst wird. Der ganze Baum wird flach ausgegeben, ein Eintrag je Datei. Nur lesend — kein Hochladen, kein Löschen.

Eine Datei hat keine numerische ID. Ihre stabile Kennung ist der Pfad relativ zur Wurzel des Auftrags, geliefert einmal lesbar als path und einmal als undurchsichtiges Token id, das in der Download-URL steht.

Antwort — 200
json
{
  "data": [
    {
      "id": "UmVjaG51bmdlbi8yMDI2L1JFLTEucGRm",
      "filename": "RE-1.pdf",
      "path": "Rechnungen/2026/RE-1.pdf",
      "mime_type": "application/pdf",
      "size": 24831,
      "created_at": "2026-06-09T11:42:13+02:00",
      "download_url": "https://<mandant>.nexoro.net/rest/v1/orders/7421/documents/UmVjaG51bmdlbi8yMDI2L1JFLTEucGRm"
    }
  ]
}
FeldBeschreibung
idDas Token des Pfads. Geben Sie es an den Download-Endpunkt weiter.
filenameDer Dateiname ohne Pfad.
pathPfad relativ zur Wurzel des Auftrags, Ordnerstruktur erhalten.
mime_typeAus dem Dateiinhalt erkannt, mit application/octet-stream als Rückfall.
sizeGröße in Bytes.
created_atISO 8601 des Änderungszeitpunkts der Datei.
download_urlVollständige URL des Download-Endpunkts für diese Datei.

Ein Auftrag ohne Ablage oder ohne Dateien liefert { "data": [] }.

StatusWann
404Der Auftrag existiert nicht.
GET/v1/orders/{id}/documents/{docId}orders:read

Eine Datei herunterladen

Liefert die Datei selbst, nicht einen JSON-Rahmen. docId ist das Token id aus der Liste. Content-Type ist der erkannte MIME-Typ, Content-Disposition ist attachment mit dem Dateinamen, Content-Length ist gesetzt, und X-Content-Type-Options: nosniff wird mitgeschickt.

Die Datei wird immer innerhalb der Wurzel des jeweiligen Auftrags aufgelöst. Ein gefälschtes Token liefert 404 — niemals eine Datei aus einem anderen Auftrag oder einem anderen Mandanten.

StatusWann
404Der Auftrag existiert nicht, hat keine Ablage, oder die Datei liegt nicht in seiner Wurzel.

Dateien im Auftrag mitliefern

GET /v1/orders/{id}?include=documents liefert dasselbe Array unter dem Schlüssel documents am Auftragsobjekt. Das ist bewusst optional, damit ein gewöhnlicher Auftragsabruf schlank bleibt.

json
{
  "data": {
    "id": 7421,
    "order_number": "EXT-7421",
    "...": "...",
    "documents": [
      { "id": "UmVjaG51bmdlbi8yMDI2L1JFLTEucGRm",
        "filename": "RE-1.pdf", "...": "..." }
    ]
  }
}

Kataloge

Die vom Mandanten definierten Kataloge, die der Auftrags-Workflow verwendet. Externe Integrationen holen sie üblicherweise einmal beim Start und arbeiten danach mit den IDs. Nur lesend, Scope catalog:read.

GET/v1/staticatalog:read

Statusdefinitionen auflisten

Der Statuskatalog des Mandanten, wie ihn die Aufträge verwenden.

Query-Parameter

NameTypStandardBeschreibung
aktiv1 / 01Nach dem Aktiv-Kennzeichen filtern. aktiv=0 zeigt archivierte Status.
ablaufint—Nach Workflow filtern: 1 ist der primäre, 2 der sekundäre.
Antwort — 200
json
{
  "data": [
    {
      "id": 1,
      "name": "Eingegangen",
      "name_partner": "Eingegangen",
      "workflow": 1,
      "sort_order": 1,
      "group": 0,
      "next_step": "Lead prüfen",
      "color": "#9ca3af",
      "priority": 5,
      "days": 0,
      "hours": 0,
      "manual": 1,
      "main_responsible": 1,
      "active": 1
    }
  ]
}
GET/v1/taskscatalog:read

Aufgabendefinitionen auflisten

Der Aufgabenkatalog des Mandanten. Jede Aufgabe ist über next_status_id mit einem Folgestatus verknüpft — darüber ermittelt POST /v1/orders/{id}/status, welchen Aufgabeneintrag es schreiben muss.

Query-Parameter

NameTypStandardBeschreibung
aktiv1 / 01Nach dem Aktiv-Kennzeichen filtern.
ablaufint—Nach Workflow filtern: 1 oder 2.
status_idint—Nur Aufgaben, deren next_status_id dieser ID entspricht.
Antwort — 200
json
{
  "data": [
    {
      "id": 1,
      "action": "Lead prüfen",
      "sort_order": 1,
      "workflow": 1,
      "next_status_id": 2,
      "help": "Vor dem Erstkontakt prüfen",
      "requires_upload": 0,
      "active": 1
    }
  ]
}

Produkte & Preise

Der Produktkatalog, seine Kategorien und die Preislisten. Nur lesend, Scope catalog:read.

GET/v1/productscatalog:read

Produkte auflisten und suchen

Query-Parameter

NameTypStandardBeschreibung
querystring—Alias q. Sucht in Name, SKU, Hersteller, Herstellernummer, GTIN, DATANORM-Artikelnummer und Beschreibung.
category_idint—Genaue Kategorie.
manufacturerstring—Exakte Übereinstimmung.
gtinstring—Exakte Übereinstimmung.
skustring—Exakte Übereinstimmung.
favorites_only1 / 00Nur als Favorit markierte Produkte.
updated_sinceISO date—Nur Produkte, deren updated_at größer oder gleich diesem Zeitpunkt ist.
limitint50Seitengröße, maximal 200.
offsetint0Versatz für die Paginierung.

Sortiert wird zuerst nach Favoriten, dann nach Name — dieselbe Reihenfolge, die der Produktkatalog in der Oberfläche zeigt.

Antwort — 200
json
{
  "data": [
    {
      "id": 11,
      "sku": "ART001",
      "name": "Ersteinbindung ERP Phase 1",
      "description": null,
      "manufacturer": null,
      "manufacturer_number": null,
      "gtin": null,
      "category_id": 2,
      "category_name": "Leistungen",
      "unit": "h",
      "selling_price": 85.0,
      "stock_quantity": null,
      "delivery_time_days": null,
      "is_favorite": true,
      "assembly_minutes": null,
      "created_at": "2026-03-02 09:12:00",
      "updated_at": "2026-06-18 11:04:00"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "total": 27 }
}
StatusWann
400updated_since konnte nicht als Datum gelesen werden.
GET/v1/products/{id}catalog:read

Ein Produkt abrufen

Dieselben Felder wie in der Liste, zusätzlich attributes und technical_specs als dekodiertes JSON — beide null, wenn sie leer oder nicht lesbar sind. 404, wenn das Produkt unbekannt ist.

GET/v1/products/{id}/pricecatalog:read

Den Verkaufspreis auflösen

Der Preis, den ein Kunde tatsächlich bekommt. Die Preisliste wird in dieser Reihenfolge bestimmt: ausdrücklich mitgegebene price_list_id, dann die Liste des Kontakts, dann die des Auftrags, dann die Standardliste. Ist das Produkt in der gewinnenden Liste nicht enthalten, gilt der Verkaufspreis am Produkt selbst.

Query-Parameter

NameTypStandardBeschreibung
order_idint—Im Kontext dieses Auftrags auflösen, also über die Liste seines Kunden.
contact_idint—Direkt für diesen Kunden auflösen.
price_list_idint—Eine bestimmte Liste erzwingen. Wird ignoriert, wenn sie inaktiv oder unbekannt ist.
quantityint1Menge, für Staffelpreise.
Antwort — 200
json
{
  "data": {
    "product_id": 11,
    "price_net": 85.0,
    "currency": "EUR",
    "source": "product",
    "price_list": {
      "id": 3, "name": "Großhandel", "currency": "EUR",
      "vat_rate": 20.0, "is_default": false
    },
    "quantity": 1
  }
}

source sagt, woher die Zahl stammt: price_list, wenn eine Liste gegriffen hat, product, wenn auf den Preis am Produkt zurückgefallen wurde, und none, wenn es überhaupt keinen Preis gibt. Zeigen oder protokollieren Sie diesen Wert immer — ein falscher Preis auf einem Angebot ist sonst ohne Debugger nicht zu erklären. price_list ist null bei Mandanten ohne Preislisten.

GET/v1/product_categoriescatalog:read

Kategorien auflisten

Flache Liste, Eltern vor Kindern, mit id, parent_id, name, description, external_code und product_count.

GET/v1/price_listscatalog:read

Aktive Preislisten auflisten

Je Liste id, name, currency, vat_rate und is_default. Bei Mandanten ohne Preislisten kommt { "data": [] } zurück — das ist ein gültiger Zustand, kein Fehler.

Kaufmännische Belege

Angebote, Auftragsbestätigungen, Rechnungen, Lieferscheine und Serviceberichte — mit Positionen und Summen.

Nicht dasselbe wie /v1/orders/{id}/documents. Dort geht es um die Dateien im Ordner eines Auftrags, hier um die Belege selbst.

{type} ist eines von offer, order_confirmation, invoice, delivery_note oder service_report. Die deutschen Bezeichnungen angebot, auftragsbestaetigung, rechnung, lieferschein und servicebericht werden ebenfalls angenommen. Der Typ ist Teil des Schlüssels und keine Zierde: IDs sind nur innerhalb eines Typs eindeutig.

Verträge sind hier nicht verfügbar und werden mit 422 abgewiesen. Sie tragen ihre eigene Intervall- und Abrechnungskonfiguration und werden in der Oberfläche gepflegt. Ebenfalls bewusst nicht über die API verfügbar: Positionsgruppen, Kalkulation, Teil- und Schlussrechnungen mit Abzügen, Mahnwesen und Textbausteine. Das sind Funktionen der Oberfläche — die API ignoriert die entsprechenden Felder nicht stillschweigend, sie bietet sie schlicht nicht an.

Die Entwurfsregel

Ein neuer Beleg ist ein Entwurf: status ist entwurf, number ist null. Die Nummer wird erst beim Bestätigen aus dem Nummernkreis des Mandanten gezogen. Das ist Absicht — ein verworfener Entwurf darf keine Lücke in einer fortlaufenden Rechnungsnummer hinterlassen.

Schicken Sie beim Anlegen und beim Bestätigen einen Idempotency-Key mit. Ein wiederholtes Anlegen bedeutet sonst einen zweiten Beleg, ein wiederholtes Bestätigen eine verbrannte Nummer.

GET/v1/commercial_documentsdocuments:read

Belege auflisten

Query-Parameter: order_id, type, status, include_inactive, limit (maximal 200) und offset. Ohne type werden alle fünf Typen zusammengeführt, neueste zuerst.

Felder je Eintrag

  • id
  • type
  • order_id
  • number
  • status
  • is_draft
  • document_title
  • net
  • gross
  • currency
  • sent
  • created_by
  • created_at
  • updated_at
POST/v1/commercial_documentsdocuments:write

Beleg anlegen

Felder

NameTypStandardBeschreibung
typePflichtstring—Siehe oben.
order_idPflichtint—Muss ein aktiver Auftrag sein.
positions[]Pflichtarray—Mindestens eine Position, siehe unten.
confirmboolfalsetrue zieht die Nummer sofort. Standard ist ein Entwurf.
datedateheuteBelegdatum.
valid_untildate—Standard: Belegdatum plus die konfigurierte Gültigkeitsdauer des Mandanten, üblicherweise 14 Tage.
document_titlestring—Optionaler Titel je Beleg.
currencystring—Standard: die Währung der Firma hinter dem Auftrag.
tax_ratenumber—Standard: der Steuersatz der Firma, üblicherweise 20.
tax_regulationstring—Eines von mwst_ausweisen, kleinunternehmer, reverse_charge, innergemeinschaftlich, nicht_steuerbar. Standard: die Einstellung des Kunden.
price_list_idint—Erzwingt eine Preisliste beim Auflösen der Positionspreise.
text_above / text_belowstring—Freier Text über bzw. unter den Positionen.
letterheadstring—Adressblock. Bei invoice nicht verfügbar.
work_descriptionstring—Nur bei service_report.
source_document_idint—Der Ursprungsbeleg, etwa das Angebot zu einer Auftragsbestätigung.

Positionen

NameTypStandardBeschreibung
product_idint—Optional. Ist es gesetzt, werden name, unit, description und der Preis aus dem Katalog befüllt.
namestring—Erforderlich, sofern kein product_id angegeben ist.
descriptionstring—Langtext unterhalb der Position.
quantitynumber1Muss größer als 0 sein.
unitstringSTOder die Einheit des Produkts.
unit_pricenumber—Nettopreis je Einheit. Weggelassen wird er über die Preisliste aufgelöst, was ein product_id voraussetzt.
tax_ratenumber—Steuersatz je Position. Standard ist der des Belegs.
is_optionalbool—Wird auf dem Beleg gezeigt, zählt aber nicht in die Summen.
quantity_plannednumber—Nur bei service_report: die geplante Sollmenge.

Ein negativer unit_price ist die Art, einen Rabatt auszudrücken.

?dry_run=1 liefert das vollständig geprüfte und gerechnete Ergebnis unter data.preview und schreibt nichts. Es rechnet mit derselben Arithmetik wie das echte Anlegen — genau das sollte eine Bestätigungsmaske anzeigen.

Antwort — 201 mit dem Beleg, so wie ihn ein GET liefern würde: inklusive positions, totals mit net, gross und by_rate sowie der Kunden-Momentaufnahme.

StatusWann
422Jede Art von Validierungsproblem: unbekannter Typ, inaktiver Auftrag, leere Positionen, unbekanntes Produkt, Menge kleiner oder gleich 0, oder eine Position ohne auflösbaren Preis.
GET/v1/commercial_documents/{type}/{id}documents:read

Einen Beleg abrufen

Der vollständige Beleg: Kopfdaten, die Kunden-Momentaufnahme, positions ohne die Summenzeilen und totals, aus den gespeicherten Summenzeilen zurückgelesen.

PATCH/v1/commercial_documents/{type}/{id}documents:write

Beleg ändern

Nimmt dieselben Felder wie das Anlegen, außer type und order_id.

positions ersetzt die gesamte Liste — es gibt keine zeilenweise Zusammenführung. Summen und Summenzeilen werden neu berechnet.

StatusWann
409Der Beleg wurde bereits versendet, oder es handelt sich um einen bestätigten Lieferschein — der dokumentiert eine Übergabe und wird nicht mehr geändert.
DELETE/v1/commercial_documents/{type}/{id}documents:write

Beleg löschen

Soft-Delete, genau wie in der Oberfläche. 204 bei Erfolg, 404 wenn der Beleg unbekannt oder bereits inaktiv ist. Die Positionen bleiben erhalten.

POST/v1/commercial_documents/{type}/{id}/confirmdocuments:write

Beleg bestätigen

Macht aus dem Entwurf einen Beleg mit Nummer und endgültigem Status und liefert ihn zurück.

Das ist der einzige Aufruf, der eine Nummer verbraucht. 409, wenn der Beleg bereits eine Nummer hat oder kein Entwurf ist.

POST/v1/commercial_documents/{type}/{id}/pdfdocuments:write

PDF erzeugen

Erzeugt das PDF mit der Vorlage des Mandanten und legt es im Dateiordner des Auftrags ab. Zurück kommt { path, filename, size }.

?inline=1 liefert das PDF direkt als application/pdf zurück und speichert nichts. Nutzen Sie das für Vorschauen, damit ein unbestätigter Beleg nie im Kundenordner landet.

Ein technisches Limit ist erwähnenswert: nur ein PDF je Request. Erzeugen Sie ein zweites als eigenen Request.

StatusWann
409Der Auftrag hat keinen Dateiordner — es wird keiner nachträglich angelegt.
POST/v1/commercial_documents/{type}/{id}/senddocuments:send

Beleg versenden

Dieser Aufruf verlangt documents:send, bewusst getrennt von documents:write: Es ist der einzige Aufruf, der beim Kunden ankommt.

Felder

NameTypStandardBeschreibung
tostring / array—Empfänger. Standard ist die Adresse am Auftrag.
ccstring / array—Optional.
subjectstring—Standard: Typ und Nummer. Die Auftragsnummer wird in eckigen Klammern angehängt, damit Antworten zugeordnet werden können.
bodystring—HTML. Standard ist ein schlichter Begleittext.
from_account_idint—Das Absenderkonto. Standard ist das erste konfigurierte.
attach_pdfbooltrueErzeugt das PDF und hängt es an.
allow_foreign_recipientboolfalseErforderlich, um an eine Adresse zu senden, die nicht am Auftrag hinterlegt ist.

Empfänger werden standardmäßig aus dem Auftrag aufgelöst, und eine ausdrücklich genannte Adresse wird gegen die zu diesem Auftrag bekannten Adressen geprüft, sofern allow_foreign_recipient nicht gesetzt ist. Diese Absicherung greift genau dann, wenn der Aufrufer ein Assistent ist, der auf Text reagiert, den er nicht selbst verfasst hat. Setzen Sie das Kennzeichen nur dort, wo der Empfänger aus einer Quelle stammt, der Sie vertrauen.

Nur SMTP-Konten werden unterstützt. Ein Postfach, das für Microsoft Graph konfiguriert ist, wird mit einer klaren Meldung abgewiesen, statt unter der falschen Identität zu versenden.

Ein Entwurf kann nicht versendet werden (409) — bestätigen Sie ihn zuerst. Bei Erfolg wird der Beleg als versendet markiert und die Nachricht protokolliert, sodass sie im Kommunikationsverlauf des Auftrags erscheint.

StatusWann
409Der Beleg ist noch ein Entwurf.
502Der Mailserver hat die Nachricht abgelehnt.

Partner

Ein Partner ist der B2B-Vertragsdatensatz eines Unternehmens: kaufmännische Konditionen, Partnerstufe, Laufzeiten.

Der Partner trägt Name, E-Mail und Telefonnummer des Unternehmens nicht selbst — die liegen an den verknüpften Kontakten. Ein Partner kann viele davon haben: typischerweise einen Datensatz, der das Unternehmen selbst abbildet, plus mehrere Ansprechpartner.

Um die Personen bei einem Partner anzulegen oder zu ändern, nutzen Sie die /v1/contacts-Endpunkte mit gesetztem partner_id. Die Partner-Endpunkte fassen nur den Vertragsdatensatz an.

Jeder Kontakt, den die Partner-Endpunkte liefern, trägt seine roles — die Funktion, die diese Person in Ihren Prozessen hat. Weil ein Ansprechpartner ein gewöhnlicher Kontakt ist, braucht es dafür kein partnerspezifisches Modell: hier lesen, über PATCH /v1/contacts/{id} schreiben.

GET/v1/partnerspartners:read

Partner auflisten

Query-Parameter

NameTypStandardBeschreibung
limitint50Seitengröße, maximal 200.
offsetint0Versatz für die Paginierung.
partner_numberstring—Filter auf exakte Übereinstimmung.
partner_typestring—Filter auf exakte Übereinstimmung, etwa Vertriebspartner, Servicepartner oder Lieferant.
updated_sinceISO date—Nur Partner, deren updated_at größer oder gleich diesem Zeitpunkt ist.
include_inactive1 / 00Liefert zusätzlich soft-gelöschte Partner.

Jede Zeile trägt ein denormalisiertes display_company — den ersten nicht leeren Firmennamen aus den verknüpften Kontakten — sowie contact_count, damit eine gewöhnliche Listendarstellung nicht je Zeile einen zweiten Aufruf braucht.

Antwort — 200
json
{
  "data": [
    {
      "id": 79,
      "partner_number": "P000004",
      "partner_type": "Servicepartner",
      "partner_tier": "Gold",
      "active": 1,
      "display_company": "Acme GmbH",
      "contact_count": 3,
      "...": "..."
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "total": 12 }
}
GET/v1/partners/{id}partners:read

Einen Partner abrufen

Liefert den Partner samt seinem contacts-Array — nur aktive Kontakte, der Datensatz des Unternehmens zuerst.

Antwort — 200
json
{
  "data": {
    "id": 79,
    "partner_number": "P000004",
    "partner_type": "Servicepartner",
    "display_company": "Acme GmbH",
    "contacts": [
      { "id": 8213, "first_name": null, "last_name": null,
        "company": "Acme GmbH", "email": "office@acme.example",
        "...": "..." },
      { "id": 8214, "first_name": "Anna", "last_name": "Müller",
        "company": null, "email": "a.mueller@acme.example",
        "position": "Geschäftsführung", "...": "..." }
    ],
    "...": "..."
  }
}
POST/v1/partnerspartners:write

Partner anlegen, optional samt Kontakten

Legt den Vertragsdatensatz an. Optional werden in derselben Transaktion ein oder mehrere verknüpfte Kontakte mit angelegt, indem Sie ein contacts-Array mitschicken. Jeder Eintrag ist ein vollständiges Kontaktobjekt mit derselben Form und derselben Prüfung wie bei POST /v1/contacts. Die ID des neuen Partners wird jedem verschachtelten Kontakt aufgeprägt; ein dort mitgeschicktes partner_id wird ignoriert.

Scheitert ein verschachtelter Kontakt an der Prüfung, wird der gesamte Aufruf zurückgerollt — weder der Partner noch irgendein Kontakt bleibt bestehen.

Schreibbare Felder am Partner

  • partner_number
  • accounting_number
  • partner_type
  • partner_tier
  • registration_number
  • website
  • note
  • title
  • discount
  • discount_code
  • acquired_by
  • partnership_started_at
  • partnership_ended_at
  • webinar_date
  • webinar_type
  • send_sms
  • send_email
  • territorial_protection
  • exclusivity

Wird partner_type weggelassen, gilt Vertriebspartner.

Anfrage — nur Partner
json
{
  "partner_number": "P-EXT-001",
  "partner_type": "Servicepartner",
  "website": "https://example.com"
}
Anfrage — Partner mit Firma und Ansprechpartner in einem Aufruf
json
{
  "partner_number": "P-EXT-002",
  "partner_type": "Servicepartner",
  "website": "https://acme.example",
  "contacts": [
    {
      "company": "Acme GmbH",
      "email": "office@acme.example",
      "phone": "+43 1 12345678",
      "street": "Hauptstraße 1",
      "postal_code": "1010",
      "city": "Wien",
      "country": "AT"
    },
    {
      "first_name": "Anna",
      "last_name": "Müller",
      "position": "Geschäftsführung",
      "email": "anna.mueller@acme.example",
      "mobile": "+43 660 1234567",
      "endpoints": [
        { "type": "phone", "value": "+43 1 12345678" }
      ]
    }
  ]
}

Antwort — 201 mit dem angelegten Partner und dem Location-Header. Wurden Kontakte verschachtelt mitgeschickt, trägt die Antwort zusätzlich ein created_contact_ids-Array und die vollständige contacts-Liste in derselben Form wie GET /v1/partners/{id}.

StatusWann
422contacts ist kein Array, oder ein verschachtelter Kontakt hat die Prüfung nicht bestanden — etwa weil keines von email, phone oder mobile vorhanden war.
PATCH/v1/partners/{id}partners:write

Partner ändern

Teilweise Aktualisierung. Jede Änderung wird in der Änderungshistorie des Partners protokolliert, updated_at wird ausdrücklich gesetzt.

DELETE/v1/partners/{id}partners:write

Partner löschen

Standardverhalten ist ein Soft-Delete: Der Partner wird inaktiv gesetzt und ein Eintrag in der Änderungshistorie geschrieben. Verknüpfte Kontakte bleiben unberührt — löschen Sie sie bei Bedarf einzeln über /v1/contacts/{id}.

Mit ?hard=1 (oder ?hard=true) werden der Partner und seine Änderungshistorie endgültig entfernt. Verknüpfte Kontakte werden nicht gelöscht, ihr Verweis auf den Partner bleibt so, wie er ist. Das ist nicht rückgängig zu machen.

StatusWann
404Der Partner existiert nicht.
409Das endgültige Löschen ist durch eine bestehende Beziehung in der Datenbank blockiert.
GET/v1/partners/{id}/contactspartners:read

Die Kontakte eines Partners

Genau das contacts-Array aus dem Einzelabruf des Partners, nur ohne die übrigen Partnerdaten. Jeder Eintrag trägt seine roles. In Kombination mit GET /v1/contacts?role=Bauleiter suchen Sie über alle Partner hinweg statt innerhalb eines einzelnen.