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
Grundsatz
Beschreibung
JSON
Anfragen und Antworten sind application/json; charset=utf-8.
snake_case
Alle 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 8601
Datums- und Zeitangaben werden als 2026-05-07T14:30:00Z erwartet. Gängige andere Schreibweisen werden ebenfalls akzeptiert, ISO 8601 ist die verlässliche.
data
Erfolgreiche Antworten liegen immer unter data, Listen zusätzlich mit einem pagination-Objekt daneben.
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
Status
Wann
401
Der 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.
403
Der 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.
Scope
Beschreibung
contacts:read / contacts:write
Kontakte lesen bzw. schreiben.
orders:read / orders:write
Aufträge lesen bzw. schreiben, einschließlich Status, Kommunikation und angehängter Dateien.
partners:read / partners:write
Partner lesen bzw. schreiben.
catalog:read
Status- und Aufgabenkatalog sowie Produkte, Kategorien und Preislisten.
documents:read / documents:write
Kaufmännische Belege (Angebot, Rechnung, …) lesen bzw. schreiben, inklusive Bestätigen und PDF-Erzeugung.
documents:send
Einen 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:
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.
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.
Status
Wann
200
Erfolg, mit Body.
201
Angelegt. Der Location-Header zeigt auf die neue Ressource.
204
Erfolg, ohne Body.
400
Ein Parameter war unbrauchbar, typischerweise ein nicht lesbares Datum.
401
Nicht authentifiziert — siehe Authentifizierung.
403
Authentifiziert, aber der nötige Scope fehlt — siehe Scopes & Rechte.
404
Die Ressource existiert nicht oder ist soft-gelöscht.
405
Die HTTP-Methode ist für diesen Pfad nicht vorgesehen.
409
Konflikt mit dem aktuellen Zustand — ein bereits versendeter Beleg, ein durch eine Beziehung blockiertes Löschen, oder ein noch laufender Request mit demselben Idempotency-Key.
422
Der Body war syntaktisch in Ordnung, hat aber die Validierung nicht bestanden.
500
Unerwarteter 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.
Name
Typ
Standard
Beschreibung
limit
int
50
Seitengröße. Auf den meisten Endpunkten bei 200 gedeckelt, beim Kommunikationsverlauf bei 500.
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.
Fall
Beschreibung
Erster Aufruf
Wird normal verarbeitet. Die Antwort wird 30 Tage lang gespeichert.
Wiederholung, gleicher Body
Die 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äuft
409 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 Body
422 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.
updated_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:
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.
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.
Status
Wann
404
Der 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.
Antwort — 200 mit dem vollständigen aktualisierten Kontakt inklusive endpoints.
Status
Wann
404
Der Kontakt existiert nicht oder ist soft-gelöscht (active = 0).
422
Kein 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.
Status
Wann
404
Der Kontakt existiert nicht.
409
Das 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.
Feld
Beschreibung
contacts
Der Stammdatensatz. Langlebig, die Quelle der Wahrheit dafür, wer jemand ist. Verwaltet über /v1/contacts.
customers
Eine 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:
Direkt lesbare Zeiger auf den aktuellen Status und die aktuelle Aufgabe, von der API konsistent gehalten.
sections
Die 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.
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.
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.
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.
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.
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
Name
Typ
Standard
Beschreibung
limit
int
50
Seitengröße, maximal 200.
offset
int
0
Versatz für die Paginierung.
customer_id
int
—
Filter auf exakte Übereinstimmung.
customer_email
string
—
Exakte Übereinstimmung auf der E-Mail-Adresse der Rechnungs-Momentaufnahme.
contact_email
string
—
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_id
int
—
Filter auf exakte Übereinstimmung.
order_number
string
—
Filter auf exakte Übereinstimmung.
status_id
int
—
Nur Aufträge, deren aktueller Status dieser ID entspricht.
updated_since
ISO date
—
Nur Aufträge, deren updated_at größer oder gleich diesem Zeitpunkt ist.
include_inactive
1 / 0
0
Liefert zusätzlich soft-gelöschte Aufträge.
include
csv
—
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.
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
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.
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.
Status
Wann
404
Der Auftrag existiert nicht oder ist soft-gelöscht.
422
custom_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.
Status
Wann
404
Der Auftrag existiert nicht.
409
Das 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.
Status
Wann
404
Der Auftrag existiert nicht.
422
status_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.
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
Name
Typ
Standard
Beschreibung
type
csv
alle
Kanäle einschränken. Beliebige Teilmenge von email, whatsapp, call — etwa type=email,call.
direction
string
beide
inbound oder outbound.
since
ISO date
—
Nur Einträge ab diesem Zeitpunkt.
limit
int
50
Seitengröße, maximal 500.
offset
int
0
Versatz innerhalb des vereinigten Verlaufs.
meta je Kanal
Kanal
Beschreibung
email
body, html, folder, kontoid, contact_id, unread, answered, forwarded, read_date sowie attachments, wenn die Nachricht über das CRM verschickt wurde.
whatsapp
message, whatsapp_number_id, endpoint_id sowie unread bei eingehenden Nachrichten.
call
duration_seconds, ring_seconds, caller_type, answer_time, hangup_time, notes, contact_id, partner_id, herkunft sowie caller_email bzw. callee_email.
Status
Wann
404
Der Auftrag existiert nicht.
400
since 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.
Das Token des Pfads. Geben Sie es an den Download-Endpunkt weiter.
filename
Der Dateiname ohne Pfad.
path
Pfad relativ zur Wurzel des Auftrags, Ordnerstruktur erhalten.
mime_type
Aus dem Dateiinhalt erkannt, mit application/octet-stream als Rückfall.
size
Größe in Bytes.
created_at
ISO 8601 des Änderungszeitpunkts der Datei.
download_url
Vollständige URL des Download-Endpunkts für diese Datei.
Ein Auftrag ohne Ablage oder ohne Dateien liefert { "data": [] }.
Status
Wann
404
Der 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.
Status
Wann
404
Der 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.
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
Name
Typ
Standard
Beschreibung
aktiv
1 / 0
1
Nach dem Aktiv-Kennzeichen filtern. aktiv=0 zeigt archivierte Status.
ablauf
int
—
Nach Workflow filtern: 1 ist der primäre, 2 der sekundäre.
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
Name
Typ
Standard
Beschreibung
aktiv
1 / 0
1
Nach dem Aktiv-Kennzeichen filtern.
ablauf
int
—
Nach Workflow filtern: 1 oder 2.
status_id
int
—
Nur Aufgaben, deren next_status_id dieser ID entspricht.
updated_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
Name
Typ
Standard
Beschreibung
order_id
int
—
Im Kontext dieses Auftrags auflösen, also über die Liste seines Kunden.
contact_id
int
—
Direkt für diesen Kunden auflösen.
price_list_id
int
—
Eine bestimmte Liste erzwingen. Wird ignoriert, wenn sie inaktiv oder unbekannt ist.
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
Name
Typ
Standard
Beschreibung
typePflicht
string
—
Siehe oben.
order_idPflicht
int
—
Muss ein aktiver Auftrag sein.
positions[]Pflicht
array
—
Mindestens eine Position, siehe unten.
confirm
bool
false
true zieht die Nummer sofort. Standard ist ein Entwurf.
date
date
heute
Belegdatum.
valid_until
date
—
Standard: Belegdatum plus die konfigurierte Gültigkeitsdauer des Mandanten, üblicherweise 14 Tage.
document_title
string
—
Optionaler Titel je Beleg.
currency
string
—
Standard: die Währung der Firma hinter dem Auftrag.
tax_rate
number
—
Standard: der Steuersatz der Firma, üblicherweise 20.
tax_regulation
string
—
Eines von mwst_ausweisen, kleinunternehmer, reverse_charge, innergemeinschaftlich, nicht_steuerbar. Standard: die Einstellung des Kunden.
price_list_id
int
—
Erzwingt eine Preisliste beim Auflösen der Positionspreise.
text_above / text_below
string
—
Freier Text über bzw. unter den Positionen.
letterhead
string
—
Adressblock. Bei invoice nicht verfügbar.
work_description
string
—
Nur bei service_report.
source_document_id
int
—
Der Ursprungsbeleg, etwa das Angebot zu einer Auftragsbestätigung.
Positionen
Name
Typ
Standard
Beschreibung
product_id
int
—
Optional. Ist es gesetzt, werden name, unit, description und der Preis aus dem Katalog befüllt.
name
string
—
Erforderlich, sofern kein product_id angegeben ist.
description
string
—
Langtext unterhalb der Position.
quantity
number
1
Muss größer als 0 sein.
unit
string
ST
Oder die Einheit des Produkts.
unit_price
number
—
Nettopreis je Einheit. Weggelassen wird er über die Preisliste aufgelöst, was ein product_id voraussetzt.
tax_rate
number
—
Steuersatz je Position. Standard ist der des Belegs.
is_optional
bool
—
Wird auf dem Beleg gezeigt, zählt aber nicht in die Summen.
quantity_planned
number
—
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.
Status
Wann
422
Jede Art von Validierungsproblem: unbekannter Typ, inaktiver Auftrag, leere Positionen, unbekanntes Produkt, Menge kleiner oder gleich 0, oder eine Position ohne auflösbaren Preis.
Der vollständige Beleg: Kopfdaten, die Kunden-Momentaufnahme, positions ohne die Summenzeilen und totals, aus den gespeicherten Summenzeilen zurückgelesen.
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.
Status
Wann
409
Der Beleg wurde bereits versendet, oder es handelt sich um einen bestätigten Lieferschein — der dokumentiert eine Übergabe und wird nicht mehr geändert.
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.
Status
Wann
409
Der Auftrag hat keinen Dateiordner — es wird keiner nachträglich angelegt.
Dieser Aufruf verlangt documents:send, bewusst getrennt von documents:write: Es ist der einzige Aufruf, der beim Kunden ankommt.
Felder
Name
Typ
Standard
Beschreibung
to
string / array
—
Empfänger. Standard ist die Adresse am Auftrag.
cc
string / array
—
Optional.
subject
string
—
Standard: Typ und Nummer. Die Auftragsnummer wird in eckigen Klammern angehängt, damit Antworten zugeordnet werden können.
body
string
—
HTML. Standard ist ein schlichter Begleittext.
from_account_id
int
—
Das Absenderkonto. Standard ist das erste konfigurierte.
attach_pdf
bool
true
Erzeugt das PDF und hängt es an.
allow_foreign_recipient
bool
false
Erforderlich, 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.
Status
Wann
409
Der Beleg ist noch ein Entwurf.
502
Der 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
Name
Typ
Standard
Beschreibung
limit
int
50
Seitengröße, maximal 200.
offset
int
0
Versatz für die Paginierung.
partner_number
string
—
Filter auf exakte Übereinstimmung.
partner_type
string
—
Filter auf exakte Übereinstimmung, etwa Vertriebspartner, Servicepartner oder Lieferant.
updated_since
ISO date
—
Nur Partner, deren updated_at größer oder gleich diesem Zeitpunkt ist.
include_inactive
1 / 0
0
Liefert 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.
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.
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}.
Status
Wann
422
contacts 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.
Status
Wann
404
Der Partner existiert nicht.
409
Das 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.
Wir verwenden nur technisch notwendige Cookies sowie – mit Ihrer Einwilligung – Google Analytics und Microsoft Clarity zur Analyse der Websitenutzung. Sie können Ihre Auswahl jederzeit ändern. Cookie-Richtlinie