Integration handbook for partners
Dieses Handbuch richtet sich an Produkt- und Entwicklerteams, die ihr Werkzeug mit Set2Sell verbinden wollen — etwa Funnel- und Formular-Tools, Werbe- und Tracking-Plattformen, Telefonie, Buchhaltung oder KI-Assistenten. Es beschreibt ausschließlich Schnittstellen, die heute live sind. Jede Angabe wurde gegen den aktuellen Programmcode und per Live-Aufruf gegen die Produktionsumgebung geprüft.
1. Auf einen Blick
Set2Sell ist ein CRM für Vertriebsteams: Leads laufen als Deals durch Pipelines mit Phasen, dazu kommen Termine, Anrufe, Aufgaben, Funnels und Automationen. Für die Anbindung von außen gibt es fünf Wege:
| Weg | Richtung | Authentifizierung | Wofür |
|---|---|---|---|
| REST-API v1 | Partner → Cockpit (lesen + schreiben) | API-Key (Bearer s2s_live_…) |
Deals anlegen, lesen, ändern, löschen, zusammenführen; Setter/Closer zuordnen; Notizen schreiben; Nachrichten (E-Mail, SMS, WhatsApp) senden oder protokollieren, Anrufe protokollieren; Termine, Terminarten und Verfügbarkeit lesen und verwalten; Phasenverlauf und Aktivitäten lesen; Pipelines, Phasen, Mitglieder und eigene Felder lesen und anlegen; Webhook-Abos verwalten |
| Webhook-Abos | Cockpit → Partner | HMAC-Signatur pro Abo | Über Ereignisse informiert werden: neuer Deal, Phasenwechsel, gewonnen/verloren, Termin gebucht, Formular abgeschickt, … |
| Lead-Import-Webhook | Partner → Cockpit (nur anlegen/anreichern) | Nutzer-ID im Header | Leads aus No-Code-Tools einspielen, inklusive Dublettenerkennung und Pipeline-Auswahl per Name |
| Zapier-App | beide | API-Key | Fertige Trigger und Aktionen ohne eigene Programmierung |
| MCP-Server | KI-Client → Cockpit | OAuth 2.1 (pro Nutzer) | KI-Assistenten (z. B. Claude) bedienen das CRM eines Nutzers mit 77 Werkzeugen |
Welcher Weg passt?
- Du baust eine eigene Integration in deinem Produkt: REST-API v1 für das Schreiben und Lesen, Webhook-Abos für das Reagieren auf Ereignisse. Beides zusammen deckt den Normalfall vollständig ab.
- Du willst nur Leads abliefern und hast ein No-Code-Tool ohne eigene Auth-Logik:
Lead-Import-Webhook. Für neue, professionelle Integrationen empfehlen wir trotzdem
POST /api/v1/deals, weil dort Rate-Limit, Fehlerformat und Berechtigungen sauberer sind. - Deine Kunden nutzen Zapier: Zapier-App verlinken, fertig. Make und n8n arbeiten über deren HTTP-Module direkt mit REST-API und Webhook-Abos.
- Du baust einen KI-Agenten: MCP-Server.
2. Grundlagen
2.1 Datenmodell in acht Begriffen
| Begriff | Bedeutung | Wichtig für Integrationen |
|---|---|---|
| Workspace (intern: Team) | Ein Kundenkonto mit Mitgliedern, Pipelines, Deals. Alles ist workspace-getrennt. | Ein API-Key gehört zu genau einem Workspace. Jede Abfrage ist automatisch auf diesen Workspace begrenzt. |
| Pipeline | Ein Vertriebsprozess mit sortierten Phasen. Ein Workspace kann mehrere Pipelines haben. | Beim Anlegen eines Deals ist pipeline_id Pflicht. Eigene Felder sind pro Pipeline definiert. |
| Phase (intern: Stage) | Eine Spalte im Board, z. B. „Neu", „Termin vereinbart", „Gewonnen". Jede Phase hat einen mapped_status. |
Ein Deal wird über seine stage_id bewegt. Der Status folgt automatisch. |
| Deal | Der zentrale Datensatz: Lead, Kontakt und Verkaufschance in einem. Trägt Kontaktdaten, Wert, Tags, Notizen, eigene Felder. | Alle Lese- und Schreibwege arbeiten auf Deals. |
| Status | Einer von sechs festen Werten: offen, termin, nachfassen, ungeeignet, gewonnen, verloren. |
Nie direkt setzbar. Der Status wird immer aus der Phase abgeleitet. Wer einen Deal auf „gewonnen" setzen will, verschiebt ihn in eine Phase mit mapped_status = gewonnen. |
| Eigene Felder (Custom Fields) | Vom Kunden pro Pipeline definierte Zusatzfelder mit Typ (Text, Zahl, Auswahl, Datum, …). | Werden über ihren Namen angesprochen, exakte Schreibweise. Schreibbar über die REST-API (POST/PATCH /deals), den Lead-Import-Webhook, MCP und die Zapier-Aktionen; lesbar am Deal-Objekt und in jedem deal.*-Webhook unter custom_fields. |
| Termin / Terminart | Gebuchte Termine (aus Buchungsseiten, Funnels oder von Hand) und deren Vorlagen. | Ereignisse appointment.* in den Webhook-Abos; volle Verwaltung über MCP. |
| Funnel | Vom Kunden gebaute Landingpage-Strecke mit Formular, Quiz oder Buchung. | Ereignis funnel.submission_completed. |
2.2 Domains und Serverstandort
| Zweck | Adresse |
|---|---|
| Anwendung und alle Schnittstellen | https://cockpit.set2sell.io |
| REST-API | https://cockpit.set2sell.io/api/v1 |
| Lead-Import-Webhook | https://cockpit.set2sell.io/api/webhooks/lead-import |
| MCP-Server | https://cockpit.set2sell.io/api/mcp |
| OAuth-Autorisierungsserver (für MCP) | https://clerk.set2sell.io |
Die Anwendung läuft in Frankfurt am Main (Vercel-Region fra1), die Datenbank
liegt bei Supabase in der Region eu-central-1 (ebenfalls Frankfurt). Ausgehende
Webhook-Aufrufe kommen aus dieser Umgebung; eine feste Absender-IP-Liste gibt es nicht.
Empfänger sollten deshalb die Signatur prüfen (Abschnitt 4.4), nicht die IP.
Es gibt keine separate Sandbox-Umgebung. Für Tests legt Set2Sell auf Anfrage einen eigenen Test-Workspace an (Abschnitt 9).
2.3 Sicherheitsmodell
- Nur HTTPS. Webhook-Ziele müssen
https://sein; lokale und private Adressen werden abgelehnt. - API-Keys werden nur als SHA-256-Hash gespeichert und genau einmal im Klartext
angezeigt. Sie können jederzeit vom Workspace-Admin widerrufen werden. Nur
Workspace-Admins können Keys anlegen. Jeder Key trägt eine Stufe (nur lesen,
lesen & schreiben, voll; Abschnitt 3.2) und handelt als sein Ersteller: Verliert
der Ersteller die Admin-Rolle oder verlässt den Workspace, antwortet die API mit
403bzw. der Key wird automatisch widerrufen. - Webhook-Signaturen sind HMAC-SHA256 mit einem Secret pro Abo, das ebenfalls nur einmal angezeigt wird.
- Rate-Limits gelten pro Key (bzw. pro Person bei OAuth), pro Workspace und für teure Routen separat; sie werden über Antwort-Header sichtbar gemacht (3.2).
- Mandantentrennung ist serverseitig erzwungen: Jede Abfrage der REST-API wird auf
den Workspace des Keys gefiltert. Eine fremde
pipeline_idoderdeal_idliefert404, nie fremde Daten.
2.4 Was welcher Weg auslöst
Im Cockpit reagieren zwei getrennte Systeme auf Änderungen: Webhook-Abos (nach außen, Abschnitt 4) und die Automationen des Kunden (im Produkt: E-Mail-Strecken, Aufgaben, Phasenwechsel). Nicht jeder Entstehungsweg eines Deals stößt beide an. Diese Matrix zeigt den heutigen Stand; plane deine Integration danach.
Wenn ein Deal entsteht:
| Entstehungsweg | Webhook-Abo deal.created |
Automation „Deal: Neu erstellt" |
|---|---|---|
| Von Hand in der Oberfläche | ✓ | ✓ |
POST /api/v1/deals (auch Zapier-Aktion „Deal anlegen") |
✓ | – |
| Lead-Import-Webhook (Abschnitt 5) | – | ✓ |
| Funnel-Formular | – (stattdessen funnel.submission_completed) |
– (stattdessen Automation „Funnel: Lead bewertet") |
| Buchungsseite / Funnel-Buchung | – (stattdessen appointment.created) |
– (stattdessen Automation „Termin: Gebucht") |
MCP create_deal / import_leads |
– | – |
Wenn ein Deal die Phase wechselt:
| Weg des Phasenwechsels | Webhook-Abo deal.stage_changed (+ deal.won / deal.lost) |
Automation „Deal: Stage gewechselt" / „Deal: Status geändert" |
|---|---|---|
| Oberfläche (Board, Deal-Detail, Abschluss) | ✓ | ✓ |
PATCH /api/v1/deals/{id} (auch Zapier-Aktion „Deal aktualisieren") |
✓ | – |
Lead-Import-Webhook mit mode: "update" |
✓ | – |
MCP move_deal / update_deal mit stage_id |
✓ | ✓ |
Ein Phasenwechsel über den MCP-Server wirkt wie einer im Deal-Drawer: neben den Webhook-Abos laufen die Zuweisungsregel der Zielphase, die Team-Benachrichtigung, phasengesteuerte Sequenzen und die Automationen „Deal: Stage gewechselt" / „Deal: Status geändert".
Praktische Folge: Wenn dein Produkt Deals anlegt oder verschiebt und der Kunde erwartet, dass seine Automationen darauf reagieren, ist das heute beim Anlegen nur über den Lead-Import-Webhook gegeben, beim Verschieben nur über den MCP-Server. Über REST-API und Lead-Import-Update starten beim Verschieben keine Automationen — sprich uns an, wenn du diesen Fall hast; die Lücke ist bekannt und auf der Liste. Für deine eigenen Reaktionen auf Ereignisse sind die Webhook-Abos unabhängig davon vollständig.
3. REST-API v1
3.1 API-Key anlegen (Kundenseite)
Der Kunde erzeugt den Key selbst:
- Im Cockpit Einstellungen → Workspace → Tab „API" öffnen (nur als Workspace-Admin sichtbar).
- Neuen Key anlegen, einen Namen vergeben (z. B. den Namen deines Produkts) und eine Stufe wählen: „Nur lesen", „Lesen & schreiben" (Standard) oder „Voll" (3.2).
- Der Key
s2s_live_…wird genau einmal angezeigt. Der Kunde kopiert ihn in deine Integration.
In der Übersicht sieht der Kunde später nur die ersten Zeichen (s2s_live_ab12cd…),
das Anlegedatum und die letzte Nutzung, und kann den Key widerrufen. Ein widerrufener
Key verhält sich nach außen wie ein unbekannter Key (401).
Deals, die über einen Key angelegt werden, gehören dem Nutzer, der den Key erstellt hat.
3.2 Authentifizierung, Rate-Limit, Fehlerformat
Header bei jeder Anfrage:
Authorization: Bearer s2s_live_…
Content-Type: application/json
Vollständige Referenz: GET https://cockpit.set2sell.io/api/v1/openapi.json
(OpenAPI 3.1, stets aktuell, mit Schemas, Stufen und Fehlercodes je Operation) und
GET /api/v1/llms.txt (kompakte Übersicht). Dieses Handbuch erklärt die Zusammenhänge;
die Tabelle in 3.3 nennt die Endpunkte, 3.3a die Gruppen, die dort nur kurz stehen.
Der Key handelt als sein Ersteller. Bei jeder Anfrage wird geprüft, dass der
Ersteller noch aktives Mitglied und Workspace-Admin ist (sonst 403 forbidden_role);
gesperrte Konten oder Workspaces antworten 403 account_locked. Verlässt der Ersteller
den Workspace, wird der Key automatisch widerrufen und seine Zapier-Webhook-Abos werden
pausiert (paused_reason: creator_left).
Stufen (Scopes) pro Key. Der Kunde wählt beim Anlegen eine von drei Stufen (Standard für neue Keys: „Lesen & schreiben"); vor Einführung der Stufen angelegte Keys haben vollen Zugriff.
| Stufe | Scopes | Erlaubt |
|---|---|---|
| Nur lesen | read |
alle GET, außerdem die Such-POSTs (/deals/search, /contacts/search) |
| Lesen & schreiben | read, write |
zusätzlich POST und PATCH |
| Voll | read, write, delete |
zusätzlich Löschen und POST /deals/merge |
Fehlt die Stufe, antwortet die API mit 403 forbidden_scope, bevor etwas geändert wird.
Ausnahmen von der Faustregel (GET = read, POST/PATCH = write, DELETE = delete):
POST /deals/merge und alle DELETE-Operationen auf Feldern, Listen, Ansichten, Aufgaben
und Kontakten brauchen delete; POST /deals/{id}/restore und DELETE /deals/{id}/tags
brauchen nur write; alle /hooks-Operationen brauchen nur write, damit sich
Zapier mit „Lesen & schreiben" wieder abmelden kann. Die Stufe jeder Operation steht
in der OpenAPI. Zapier braucht mindestens „Lesen & schreiben".
OAuth statt API-Key. Neben s2s_live_…-Keys akzeptiert die API ein Clerk-OAuth-
Access-Token: Authorization: Bearer <token> plus der Pflicht-Header
X-Workspace-Id: <Workspace-UUID> (ohne ihn 422). Es gilt dieselbe Prüfung (die Person
hinter dem Token muss Admin des Workspace sein), die Stufen sind voll. Partner-Apps für
OAuth registriert Set2Sell — melde dich über den Kontakt in Abschnitt 9; eine
selbstverwaltete Registrierung gibt es nicht. (Der MCP-Server in Abschnitt 7 nutzt
denselben Autorisierungsserver, aber einen eigenen Endpunkt.) Schickt ein Key-Aufruf
einen X-Workspace-Id, der nicht zum Workspace des Keys passt, kommt 422.
Rate-Limits. Drei Eimer, der engste gewinnt:
| Eimer | Limit |
|---|---|
| pro Aufrufer (API-Key bzw. Person bei OAuth) | 600 Anfragen/Minute |
| pro Workspace | 1.800 Anfragen/Minute |
teure Routen (in der OpenAPI x-expensive: Suchen, Smart-View-Dealliste und -Zähler, Massen-Update PATCH /tasks) |
zusätzlich 60 Anfragen/Minute |
Jede Antwort — auch 4xx und 5xx — trägt diese Header; eine 429 trägt die Zahlen
des Eimers, der angeschlagen hat:
| Header | Bedeutung |
|---|---|
X-RateLimit-Limit |
Anfragen pro Minutenfenster |
X-RateLimit-Remaining |
im laufenden Fenster noch frei |
X-RateLimit-Reset |
Unix-Sekunden, wann das Fenster neu beginnt |
Retry-After |
nur bei 429: Sekunden bis zum nächsten Versuch |
Der Zähler ist über alle Server-Instanzen geteilt, das Limit gilt also global.
Zusätzlich werden fehlgeschlagene Authentifizierungen pro IP gezählt (20 pro
Minute). Wer ungültige Keys durchprobiert, bekommt 429 statt weiterer 401.
Fehlerformat (einheitlich für alle v1-Routen; details nur bei Zusatzkontext):
{ "error": { "code": "validation_error", "message": "pipeline_id: Invalid UUID" } }
| HTTP | code |
Wann |
|---|---|---|
| 401 | unauthorized |
Key/Token fehlt, ist unbekannt oder widerrufen |
| 402 | payment_required |
Credit- oder Plan-Limit des Workspace erschöpft |
| 403 | forbidden_scope |
dem Key fehlt die nötige Stufe |
| 403 | forbidden_role |
der Ersteller des Keys ist kein Workspace-Admin (mehr) |
| 403 | account_locked |
Konto oder Workspace gesperrt |
| 404 | not_found |
Deal/Pipeline/Abo gehört nicht zum Workspace des Keys oder existiert nicht |
| 409 | conflict |
z. B. Kontakt-Dublette (details.existing_id nennt den Bestandskontakt) oder Idempotency-Key noch in Bearbeitung |
| 422 | validation_error |
Ungültiger Body oder Query-Parameter, unbekanntes Feld, Phase passt nicht zur Pipeline |
| 422 | idempotency_mismatch |
Idempotency-Key mit einer anderen Anfrage wiederverwendet |
| 422 | blocked |
Versand von einer Leitplanke gestoppt oder vom Anbieter abgelehnt (details.reason, siehe „Versand“ unten) |
| 429 | rate_limited |
Limit erreicht, siehe Retry-After |
| 500 | internal_error |
Fehler auf unserer Seite (Meldung: Internal error) |
| 502 | provider_error |
Versandanbieter (E-Mail, Twilio) hat nicht oder mit einem Fehler geantwortet, der nicht an der Anfrage liegt |
Zwei Regeln, die viele Integratoren überraschen:
- Unbekannte Felder und Query-Parameter werden mit
422abgelehnt, nicht still ignoriert. Ein?page=2oder ein"status": "gewonnen"im Body ist ein Fehler. - Kein
400. Ungültiges JSON landet ebenfalls als422 validation_error.
Konventionen für Listen und POST:
fields=auf jedemGET: Feldauswahl (fields=id,contact.email); ein unbekannter Pfad →422.sort=dort, wo der Endpunkt es anbietet (-feld= absteigend). Dann istnext_cursorundurchsichtig: unverändert alscursorzurückgeben, nicht deuten.include_counts=truedort, wo angeboten: lieferttotal(bei/dealsauchtotal_value). Gezählt wird pro Anfrage — mit der ersten Seite mitschicken.- Idempotenz: Jedes
POSTakzeptiertIdempotency-Key(1–255 Zeichen; ein leerer Header gilt als fehlend). Gleicher Key + gleiche Anfrage innerhalb von 24 Stunden → die gespeicherte Antwort, mit HeaderIdempotent-Replayed: true. Gleicher Key mit anderer Anfrage →422 idempotency_mismatch; läuft das Original noch →409 conflict.5xx,429und402werden nicht gespeichert, ein Retry ist also gefahrlos. Nicht unterstützt (422, wenn der Header kommt):POST /hooks(die Antwort enthält das Einmal-Secret) sowiePOST /contacts/searchundPOST /deals/search(reine Lesevorgänge, einfach wiederholbar).
3.3 Endpunkt-Referenz
| Methode | Pfad | Zweck |
|---|---|---|
| GET | /me |
Verbindungstest, liefert Workspace, Währung, Stufen und handelnde Person |
| GET | /members |
Aktive Mitglieder des Workspace (Nutzer-IDs auflösen) |
| GET | /pipelines |
Alle Pipelines mit Phasen |
| GET | /deals |
Deals auflisten, filtern, blättern; gelöschte Deals per deleted_since |
| POST | /deals |
Deal anlegen |
| GET | /deals/{id} |
Einzelnen Deal lesen |
| PATCH | /deals/{id} |
Deal ändern, Setter/Closer setzen oder in eine andere Phase verschieben |
| DELETE | /deals/{id} |
Deal löschen (Soft-Delete, 30 Tage wiederherstellbar) |
| GET | /deals/{id}/stage-history |
Alle Phasenwechsel eines Deals |
| POST | /deals/{id}/notes |
Notiz an die Zeitleiste eines Deals schreiben |
| POST | /deals/merge |
Dubletten in einen Hauptdeal zusammenführen |
| GET | /stage-changes |
Phasenwechsel des ganzen Workspace, chronologisch, inkrementell |
| GET | /activities |
Zeitleisten-Einträge des Workspace: Anrufe, Notizen, E-Mails, Termine, Aufgaben, SMS, WhatsApp |
| GET | /appointments |
Termine mit Gastgeber, Terminart und Status |
| PATCH | /pipelines/{id}/stages/{id} |
Status-Zuordnung oder Name einer Phase ändern |
| GET | /custom-fields |
Definitionen der eigenen Felder |
| POST | /custom-fields |
Eigenes Feld anlegen |
| PATCH | /custom-fields/{id} |
Beschreibung, Pflicht-Status oder Auswahlwerte eines Feldes ändern |
| DELETE | /custom-fields/{id} |
Eigenes Feld löschen — samt aller Werte an den Deals |
| POST | /hooks |
Webhook-Abo per API anlegen (für Zapier-artige Integrationen) |
| PATCH | /hooks/{id} |
Per API angelegtes Abo ändern (Adresse, Ereignisse, Name, aktiv) — Secret bleibt |
| DELETE | /hooks/{id} |
Per API angelegtes Abo wieder entfernen |
| GET | /webhook-subscriptions |
Alle Abos des Workspace inklusive Pause-Zustand |
| GET | /webhook-subscriptions/{id}/deliveries |
Zustellprotokoll eines Abos |
Alle Pfade relativ zu https://cockpit.set2sell.io/api/v1.
3.3a Weitere Endpunktgruppen
Diese Gruppen sind in der Tabelle oben nicht einzeln aufgeführt; Parameter, Schemas und
Stufen stehen in der openapi.json.
| Gruppe | Endpunkte | Zweck |
|---|---|---|
| Kontakte | /contacts, /contacts/{id}, /contacts/{id}/deals |
Kontakte ohne Deal anlegen, lesen, ändern, löschen; Dublettenregel wie in der App (E-Mail, sonst Telefon → 409 mit details.existing_id) |
POST /contacts/search |
Suche mit dem Filterbaum der Kontaktliste | |
/contact-fields, /contact-lists |
Eigene Kontaktfelder und gespeicherte Kontaktlisten | |
/deals/{id}/contacts |
Kontakte an einem Deal lesen, verknüpfen, lösen | |
| Deal-Suche | POST /deals/search |
Suche mit dem Filterbaum der Smart Views (inkl. Aktivitätsblöcken), in der Datenbank ausgewertet |
GET /filter-catalog |
Felder, Operatoren und Auswahlwerte, aus denen sich der Filterbaum zusammensetzt | |
| Smart Views | /smart-views, /smart-views/{id}, /smart-views/{id}/deals, /smart-views/{id}/count |
Gespeicherte Ansichten verwalten, ihre Deals blättern, Treffer zählen |
| Deals | POST /deals/{id}/restore |
Gelöschten Deal wiederherstellen |
POST /deals/{id}/tags, DELETE /deals/{id}/tags |
Tags hinzufügen/entfernen | |
| Aufgaben | /tasks, /tasks/{id}, POST /tasks/{id}/complete |
Aufgaben an Deals (inkl. Nachfass-Aufgabe mit follow_up: true) |
PATCH /tasks |
Massen-Update bis 100 Aufgaben je Anfrage (teure Route) | |
| Notizen | PATCH /notes/{id}, DELETE /notes/{id} |
Text einer Notiz ersetzen bzw. Notiz löschen (DELETE braucht delete) |
| Anrufe | GET /calls, GET /calls/{id} |
Anrufliste des Workspace (die der App: eigene Telefonie, KI-Agent, per API protokollierte Anrufe) |
POST /calls |
Extern geführten Anruf an einem Deal protokollieren. Nie als Telefonminuten abgerechnet; ein dokumentiertes Ergebnis löst wie in der App die Automation „Anrufergebnis dokumentiert“ aus | |
GET /emails, GET /emails/{id} |
E-Mails (gesendet, geplant, empfangen); die Einzelabfrage liefert zusätzlich das HTML | |
POST /emails |
mode: send, schedule oder log (siehe „Versand“; teure Route) |
|
| SMS | POST /sms |
mode: send oder log (teure Route; kein schedule) |
POST /whatsapp-messages |
mode: send oder log; außerhalb des 24-Stunden-Fensters ist eine freigegebene Vorlage Pflicht (teure Route) |
|
| Gespräche | GET /conversations, GET /conversations/{id}/messages |
SMS-/WhatsApp-Gespräche wie der Posteingang und ihre Nachrichten; Lesen markiert nichts als gelesen |
| Termine | POST /appointments |
Termin buchen wie im Cockpit (siehe „Termine buchen“) |
GET /appointments/{id}, PATCH /appointments/{id} |
Termin lesen; ändern bzw. verschieben bzw. Status dokumentieren | |
DELETE /appointments/{id} |
Termin endgültig löschen (delete) — zum Absagen mit Protokoll POST …/cancel nutzen |
|
POST /appointments/{id}/cancel |
Termin absagen (Status abgesagt); wiederholbar |
|
| Terminarten | GET /event-types, POST /event-types |
Terminarten lesen und anlegen |
GET/PATCH/DELETE /event-types/{id} |
Einzelne Terminart lesen, ändern, löschen (DELETE braucht delete) |
|
POST /event-types/{id}/duplicate |
Terminart duplizieren | |
GET /event-types/{id}/slots |
Freie Zeiten wie auf der Buchungsseite, höchstens 31 Tage je Anfrage (Standard: 7 Tage; teure Route) | |
| Verfügbarkeit | GET /availability, PUT /availability |
Wochenarbeitszeiten eines Mitglieds lesen bzw. ersetzen (ohne timezone bleibt die Zeitzone des Mitglieds) |
GET /availability/overrides, POST /availability/overrides |
Ausnahmen (Urlaub, Sonderzeiten) lesen bzw. anlegen | |
DELETE /availability/overrides/{id} |
Ausnahme löschen (delete) |
|
| Pipelines | POST /pipelines |
Pipeline anlegen wie in der App: entweder mit eigenen Phasen (stages, je Phase mapped_status Pflicht) oder aus einer Vorlage (template_id, locale); ohne beides die Standardvorlage. Anruf-/Terminstatus-Zuordnungen und die „Nicht erreicht“-Kette werden aus den Phasen abgeleitet |
GET /pipeline-templates |
Vorlagen mit Phasen und Statuszuordnung, deutsch oder englisch (locale) |
|
PATCH /pipelines/{pipelineId} |
Umbenennen oder zur Standard-Pipeline machen (is_default: true; die bisherige verliert das Kennzeichen — Lead-Importe ohne Pipeline landen in der Standard-Pipeline) |
|
DELETE /pipelines/{pipelineId} |
Leere Pipeline löschen (delete): sie wandert in den Papierkorb (in der App wiederherstellbar, nach 30 Tagen endgültig weg). Mit Deals 409 (details.deal_count; Deals im Papierkorb zählen nicht), die Standard-Pipeline ebenfalls 409 |
|
POST /pipelines/{pipelineId}/stages |
Phase hinzufügen (mapped_status Pflicht, position 0-basiert, sonst ans Ende); ein schon vorhandener Phasenname → 409 |
|
PUT /pipelines/{pipelineId}/stages/order |
Reihenfolge ändern: stage_ids nennt jede Phase genau einmal, sonst 422 (details.missing / unknown / duplicate) |
|
DELETE /pipelines/{pipelineId}/stages/{stageId} |
Phase löschen (delete); 409, solange Deals darin liegen (details.deal_count), gelöschte Deals im Papierkorb noch auf sie verweisen oder es die letzte Phase der Pipeline ist |
|
PATCH /pipelines/{pipelineId}/stages/{stageId} |
Name oder mapped_status einer Phase ändern. Gelöschte oder archivierte Pipelines antworten 404, ein schon vorhandener Phasenname 409 |
|
| Sperrlisten | GET /email-suppressions, POST /email-suppressions |
E-Mail-Sperrliste des Workspace (Bounces, Abmeldungen, Spam-Beschwerden, manuelle Einträge, Kontaktverbote eines Deals mit Quelle contact_block); removable zeigt, ob ein Eintrag entfernbar ist. POST (reason: manual oder unsubscribe): 201 neu, 200 mit dem vorhandenen Eintrag. Danach geht an die Adresse keine E-Mail mehr — auch keine bereits geplante |
DELETE /email-suppressions/{email} |
Adresse (URL-kodiert) von der Liste nehmen (delete), nur Bounce- und manuelle Einträge. Steht sie auch als Abmeldung/Spam-Beschwerde oder als Kontaktverbot eines Deals auf der Liste, wird nichts entfernt: 409 (Kontaktverbot aufheben über DELETE /deals/{id}/contact-block) |
|
GET /call-blocks, POST /call-blocks |
Anrufsperren des Workspace: Nummern, die KI-Agenten und Set2Sell-Telefon nicht anrufen (SMS und WhatsApp über die API ebenso gesperrt). POST normalisiert die Nummer wie der Wähler (E.164, nationale Nummern gelten als deutsch), beendet wartende KI-Anrufe an sie samt Automationen; 201 neu, 200 mit der vorhandenen Sperre |
|
DELETE /call-blocks/{id} |
Anrufsperre aufheben (delete); der zugehörige Deal bekommt eine Verlaufsnotiz |
|
POST /deals/{id}/contact-block, DELETE /deals/{id}/contact-block |
Kontaktverbot für die Person am Deal wie der Knopf in der App: E-Mail-Adresse(n) auf die Sperrliste und/oder Nummer sperren (Kanäle email, calls; Standard beide; bei beiden enden wartende Automationsläufe). Ein Tag wie „DND“ bewirkt nichts. DELETE (delete) hebt nur diese Kontaktverbot-Einträge und die manuelle Anrufsperre auf; Bounces, Abmeldungen und ein dem KI-Agenten gesagtes „nicht mehr anrufen“ bleiben |
|
| Mitglieder | GET /members, GET /roles |
Aktive Mitglieder (id = Nutzer-ID aus owner_id, setter_id, closer_id, changed_by; ausgeschiedene fehlen) und die Rollen mit Rechten und Vergebbarkeit |
PATCH /members/{userId} |
Rolle eines aktiven Mitglieds ändern (admin, member, viewer). Der Owner ist nicht änderbar (409), die eigene Rolle des Key-Nutzers nicht (422). Die Rolle wird zuerst beim Anmeldeanbieter (Clerk), dann im Workspace geschrieben und erscheint im Team-Aktivitätsprotokoll. Mitglieder entfernen ist über die API nicht möglich (Sitzplatzabrechnung) — das geht nur in der App |
|
| Einladungen | GET /invitations, POST /invitations |
Offene Einladungen lesen; per E-Mail einladen wie in den Mitgliedereinstellungen. Der Sitzplatz-Check läuft je Adresse; data hat ein Ergebnis je Adresse (invited, already_member, already_invited, seat_limit, failed) — Teilerfolg ist kein Fehler (200) |
DELETE /invitations/{id} |
Offene Einladung dieses Workspace zurückziehen (delete); der Link in der Mail wird ungültig. Fremde oder bereits angenommene Einladungen → 404 |
|
| E-Mail-Vorlagen | GET /email-templates, GET /email-templates/{templateId} |
Vorlagen des Workspace: geteilte und die des Key-Erstellers (private anderer Mitglieder sind unsichtbar), neueste zuerst; die Einzelabfrage liefert body_html und bei Design-Vorlagen das Baustein-Dokument body_json. Vorlagen der Termin-Benachrichtigungen gehören zur Terminart und erscheinen hier nicht |
POST /email-templates, PATCH /email-templates/{templateId} |
Anlegen bzw. ändern: entweder body_json (Baustein-Dokument des Designers — das HTML rendert der Server, unbekannte Bausteine → 422 mit details.skipped) oder body_html (reines HTML; macht aus einer Design-Vorlage eine HTML-Vorlage). Kategorien erstkontakt, nachfassen, angebot, termin, custom; ohne is_shared privat |
|
DELETE /email-templates/{templateId} |
Endgültig löschen (delete). Verschickte Mails und Automations-Schritte behalten ihre Kopie |
|
POST /email-templates/{templateId}/render |
Vorschau für einen Deal (deal_id, Stufe read): Betreff und HTML mit denselben Platzhalter-Werten wie der Versand ({{vorname}}, {{nachname}}, {{firma}}, {{email}}, {{telefon}}, {{deal_name}}, {{anrede}}); {{abmelde_link}} bleibt stehen und wird beim Versand gefüllt. Den Betreff für POST /emails von hier nehmen — der Versand ersetzt Platzhalter nur im Text |
|
| SMS-Vorlagen | GET /sms-templates |
Eingebaute Vorlagen, dann die des Workspace. Nur lesen |
| WhatsApp-Vorlagen | GET /whatsapp-templates |
Vorlagen am Twilio-Konto des Workspace; standardmäßig nur sendbare (die POST /whatsapp-messages außerhalb des 24-Stunden-Fensters annimmt), mit include_pending=true auch wartende und abgelehnte (der Freigabestand wird dann bei Twilio aufgefrischt). Angelegt werden Vorlagen in der App |
| Automationen | GET /automations, GET /automations/{automationId} |
Automationen (neueste zuerst, ohne status alle außer archivierten); die Einzelabfrage liefert den Ablauf { nodes, edges } der neuesten Version — des Entwurfs, wenn er neuer ist als die veröffentlichte (version.published) |
POST /automations, PATCH /automations/{automationId} |
Anlegen bzw. Ablauf ändern — immer als Entwurf: eine aktive Automation läuft auf der veröffentlichten Version weiter. Geprüft wie im Builder; ein unvollständiger Ablauf → 422 mit allen Befunden in details.errors. email.send darf eine Vorlage per template_id nennen (geteilte oder eigene des Key-Erstellers). Code-Knoten gehen nur im Builder. Archivierte (in der App gelöschte) → 409 |
|
POST /automations/{automationId}/activate |
Neueste Version veröffentlichen und scharf schalten: ab jetzt schreibt der Auslöser echte Datensätze ein, und die Schritte versenden wirklich. Geprüft wie „Veröffentlichen“ im Builder (Ablauf, Einstellungen jedes Schritts, Ziel) — ein Befund → 422 mit details.errors, die Automation bleibt, wie sie war. Setzt auch eine pausierte fort. Eine eigene Absenderadresse braucht dieselbe Freigabe wie im Builder (sonst 403) |
|
POST /automations/{automationId}/pause |
Wie der Schalter in der App: niemand Neues wird eingeschrieben; wer schon drin ist, läuft weiter (einzeln beenden über die Läufe des Deals). Ein Entwurf → 409 |
|
GET /automation-capabilities |
Alle Auslöser und Aktionen mit dem JSON Schema ihrer config und ihren benannten Ausgängen, dazu die Operatoren von Bedingungen — derselbe Katalog wie Builder und CoPilot |
|
| Läufe eines Deals | GET /deals/{id}/automation-runs |
Automationen, in denen der Deal gerade steckt (wartend oder im Schritt, ohne Testläufe), mit nächstem Schritt und Zeitpunkt |
DELETE /deals/{id}/automation-runs/{runId} |
Deal aus einer Automation nehmen wie „Entfernen“ in der App (delete): der Lauf endet, ein wartender KI-Anruf wird abbestellt, der Verlauf hält fest, wer es war. Während eines Schritts 409 mit details.reason: laeuft_gerade (in Sekunden wiederholen), schon beendet 409 schon_beendet |
Sequenzen im Sinne von Close bildet Set2Sell mit Automationen ab; einen eigenen /sequences-Endpunkt gibt es nicht.
Alle Operationen dieser Gruppen (Pipelines, Sperrlisten, Mitglieder, Einladungen, Vorlagen, Automationen) setzen einen Workspace-Admin als Key-Ersteller voraus (siehe 3.2) und brauchen die Stufe aus der Tabelle in 3.2: GET = read, POST/PATCH/PUT = write, DELETE = delete — Ausnahme: POST /email-templates/{templateId}/render liest nur (read).
Versand (E-Mail, SMS, WhatsApp)
POST /emails, POST /sms und POST /whatsapp-messages laufen über denselben Weg wie
der Versand aus dem Cockpit (Absender, Signatur, Kontingent, Zeitleiste) und zusätzlich
durch dieselben Leitplanken. Alle drei liegen im teuren Rate-Limit-Eimer (60/Minute, 3.2)
und brauchen die Stufe write.
mode:
| Wert | Wirkung |
|---|---|
send |
sendet sofort |
schedule |
nur E-Mail: sendet zu scheduled_at. SMS und WhatsApp kennen kein Planen |
log |
sendet nichts und kostet nichts. Hält eine anderswo gesendete oder empfangene Nachricht als Zeitleisten-Eintrag fest. Mit external_id liefert eine zweite Anfrage am selben Deal den vorhandenen Eintrag (200). Protokollierte E-Mails erscheinen in GET /activities, nicht in GET /emails; protokollierte SMS/WhatsApp nicht in GET /conversations |
Leitplanken, in dieser Reihenfolge — alle vor dem Versand, bei einem Treffer wird nichts gesendet oder gezählt:
- Kontaktverbot. Steht der Empfänger auf der Sperrliste „Nicht kontaktieren“ des
Workspace →
422 blocked,details.reason: contact_block. Bei SMS und WhatsApp wird die Nummer in allen Schreibweisen verglichen (079…,+4179…,0049…). Das ist strenger als die App, die dort heute nicht prüft. - Sperrliste (E-Mail). Adresse auf der Unterdrückungsliste (Bounces, Beschwerden)
→
422 blocked,reason: suppressed. - Absender. E-Mail braucht ein Absenderkonto des Workspace mit verifizierter Domain;
SMS/WhatsApp einen aktiven Kanal →
422 validation_error. - Abo und Kontingent. Ohne aktives Abo oder bei erschöpftem Kontingent
→
402 payment_required. (Das E-Mail-Kontingent wird beim tatsächlichen Versand geprüft, beischedulealso zuscheduled_at; die Mail steht dann auffailed.) - WhatsApp-Vorlage und 24-Stunden-Fenster. Freitext (
body) ist nur innerhalb von 24 Stunden nach der letzten Nachricht des Kontakts erlaubt; eine Erstnachricht braucht immer eine freigegebene Vorlage (template_id+template_variables, alle Platzhalter gefüllt) → sonst422 blocked,reason: whatsapp_window_closed(fehlende oder unvollständige Vorlage:422).
Empfängernummer. SMS und WhatsApp normalisieren die Nummer wie die App zu E.164
(nationale Schreibweisen werden mit der Landesvorwahl der Absendernummer des Kanals
gelesen, sonst +49). Ist sie nicht wählbar → 422 validation_error mit Feld to.
Geprüft und gesendet wird an genau dieselbe Nummer.
Anbieter-Ablehnung. Lehnt Twilio dauerhaft ab (ungültige Nummer, Empfänger hat mit
STOP abgemeldet, 4xx außer 401/403/429) → 422 blocked, reason: provider_rejected,
details.provider_code. Dieselbe Anfrage scheitert wieder, deshalb wird die Antwort
gespeichert. Bei 401/403/429, 5xx oder wenn der Anbieter nicht antwortet → 502 provider_error
(nicht gespeichert).
Idempotenz. Sende bei jedem Versand einen Idempotency-Key: eine Wiederholung mit
demselben Key liefert die gespeicherte Antwort und sendet nicht erneut — auch dann,
wenn der Anbieter angenommen, das Speichern aber gescheitert ist (SMS/WhatsApp: message
ist dann null). 402 und 5xx werden nicht gespeichert. Der einzige Fall, in dem
eine Wiederholung erneut senden kann: ein 502, bei dem der Anbieter nicht geantwortet
hat — er kann die Nachricht trotzdem zugestellt haben.
Termine buchen
POST /appointments bucht wie das Cockpit (Deal-Drawer, Kalender): Bestätigungsmail und
Erinnerungen der Terminart (an den Kontakt des Deals, sonst booked_by_email/
booked_by_phone), Mail an den Gastgeber (falls nicht du selbst), die Webhooks
appointment.* (4.2), Automationen „Termin gebucht“, Ort und Videoraum der Terminart und
der Kalender-Push zum verbundenen Kalender des Gastgebers. Gebucht wird mit
booking_source internal. Ein aktives Abo ist nötig (402).
- Gastgeber:
host_id, sonst der Gastgeber der Terminart (Round Robin unter ihren Mitgliedern), sonst der Ersteller des Keys. - Überschneidung: belegt der Gastgeber die Zeit schon →
409 conflict. - Außerhalb der Arbeitszeiten, an einem freien Tag oder über dem Tageslimit wird der
Termin trotzdem gebucht; die Antwort trägt ein
warning. Belegte Zeiten in externen Kalendern werden nur bei der Wahl des Round-Robin-Mitglieds geprüft — rufe vorherGET /event-types/{id}/slotsab. - Round Robin: ist zur gewünschten Zeit kein Mitglied frei →
409; hat die Terminart keine aktiven Mitglieder →422. - Idempotenz: sende einen
Idempotency-Key. Ohne ihn bucht ein Wiederholungsversuch erneut (bei Round Robin womöglich bei einem anderen Mitglied). - Absagen:
POST /appointments/{id}/cancelsetzt den Status aufabgesagt(der Termin bleibt als Nachweis), stoppt Erinnerungen, benachrichtigt den Gebuchten, entfernt den Kalendereintrag und feuertappointment.cancelled. Ein bereits abgesagter Termin kommt unverändert zurück, ohne zweite Benachrichtigung — Wiederholen ist gefahrlos.DELETE /appointments/{id}löscht dagegen endgültig und sendet keinen Webhook. - Verschieben:
PATCHmit neuerstart_time/end_timeaktualisiert den Kalender, benachrichtigt den Gebuchten und feuertappointment.rescheduled; eine belegte Zeit →409.
Änderung am Vertrag (29.10.2026):
next_cursorvonGET /appointments. Der Cursor ist jetzt eine undurchsichtige Zeichenkette statt der Termin-ID. Gibnext_cursorunverändert alscursorzurück und werte ihn nicht aus. Ein alter Cursor in Form einer Termin-UUID wird weiter angenommen. Der Cursor gilt nur für dieselbe Sortierung (order): ein Cursor ausascmitorder=desc(oder umgekehrt) →422.
GET/me
curl https://cockpit.set2sell.io/api/v1/me \
-H "Authorization: Bearer s2s_live_…"
{
"data": {
"team": { "id": "3f2c…", "name": "Beispiel GmbH", "currency": "EUR" },
"scopes": ["read", "write"],
"user": { "id": "user_3JB…", "role": "admin" }
}
}
scopes sind die Stufen des Aufrufers (["read"], ["read","write"] oder
["read","write","delete"]), user die handelnde Person: der Ersteller des Keys bzw.
der angemeldete OAuth-Nutzer.
currency ist der ISO-4217-Code der Workspace-Währung; alle value-Beträge von Deals
sind in dieser Währung.
GET/members
Aktive Mitglieder des Workspace. id ist die Nutzer-ID (user_…), unter der Deals
owner_id, setter_id und closer_id führen und Phasenwechsel changed_by. Die
Zuordnung zu Personen in deinem System läuft am besten über email.
curl https://cockpit.set2sell.io/api/v1/members \
-H "Authorization: Bearer s2s_live_…"
{
"data": [
{ "id": "user_3JB…", "name": "Anna Beispiel", "email": "anna@example.com",
"role": "admin", "joined_at": "2026-01-15T09:30:00.000Z" }
]
}
role ist owner | admin | member | viewer. Ausgeschiedene Mitglieder erscheinen
nicht mehr — wer den Zugang in deinem System an die Mitgliedschaft koppelt, gleicht
regelmäßig gegen diese Liste ab. IDs, die in älteren Deals oder im Phasenverlauf
stehen, aber hier fehlen, gehören ehemaligen Mitgliedern.
GET/pipelines
Liefert alle aktiven (nicht gelöschten, nicht archivierten) Pipelines, sortiert nach Position, jeweils mit ihren Phasen.
{
"data": [
{
"id": "8a1e…", "name": "Vertrieb", "is_default": true,
"stages": [
{ "id": "c0d1…", "name": "Neu", "position": 0, "mapped_status": "offen" },
{ "id": "c0d2…", "name": "Termin", "position": 1, "mapped_status": "termin" },
{ "id": "c0d3…", "name": "Gewonnen", "position": 2, "mapped_status": "gewonnen" },
{ "id": "c0d4…", "name": "Verloren", "position": 3, "mapped_status": "verloren" }
]
}
]
}
Die mapped_status-Werte brauchst du, um zu wissen, welche Phase einen Deal auf
„gewonnen" oder „verloren" setzt.
PATCH/pipelines/{pipelineId}/stages/{stageId}
Status-Zuordnung (mapped_status) oder name einer Phase ändern. Damit gleicht ein
Partner ab, welche Phase als offen, gewonnen oder verloren zählt.
curl -X PATCH https://cockpit.set2sell.io/api/v1/pipelines/8a1e…/stages/c0d3… \
-H "Authorization: Bearer s2s_live_…" -H "Content-Type: application/json" \
-d '{ "mapped_status": "gewonnen" }'
Antwort: { "data": { "id", "pipeline_id", "name", "position", "mapped_status" } }.
mapped_status nimmt die sechs Status-Werte aus 11.1. Deals, die bereits in der Phase
stehen, behalten ihren Status, bis sie das nächste Mal verschoben werden — wie beim
Umbau im Pipeline-Editor. 404, wenn Pipeline oder Phase nicht zum Workspace gehören.
GET/deals
Query-Parameter (alle optional; unbekannte Parameter → 422):
| Parameter | Typ | Bedeutung |
|---|---|---|
pipeline_id |
UUID | nur Deals dieser Pipeline |
stage_id |
UUID | nur Deals dieser Phase |
status |
offen|termin|nachfassen|ungeeignet|gewonnen|verloren |
nach Status filtern |
updated_since |
ISO-8601 mit Zeitzone | nur Deals mit updated_at >= Wert |
deleted_since |
ISO-8601 mit Zeitzone | nur seither gelöschte Deals (deleted_at >= Wert); ohne diesen Parameter sind gelöschte Deals nie enthalten |
owner_id, setter_id, closer_id |
Nutzer-ID aus GET /members |
nur Deals dieser Person in der jeweiligen Rolle |
search |
Text, max. 200 Zeichen | Teilstring-Suche über Titel, Kontaktname, E-Mail, Telefon, Firma |
limit |
1–100, Standard 25 | Seitengröße |
cursor |
UUID | Fortsetzung, Wert aus next_cursor der vorigen Antwort |
{ "data": [ …Deal… ], "next_cursor": "9b7f…" }
Blättern: Solange next_cursor nicht null ist, die Anfrage mit
?cursor=<wert> wiederholen. Die Reihenfolge ist stabil, aber nicht chronologisch
(sortiert nach id). Für „alles seit Zeitpunkt X" deshalb updated_since verwenden,
nicht die Cursor-Reihenfolge.
Löschungen erkennen: Ein gelöschter Deal verschwindet aus allen Antworten, auch aus
updated_since-Abfragen. Wer Löschungen nachvollziehen will, pollt zusätzlich
GET /deals?deleted_since=<letzter Lauf> — die Antwort trägt dann deleted_at. Das
ergänzt das Ereignis deal.deleted, falls eine Zustellung verloren ging. Löschungen
sind 30 Tage nachvollziehbar; ein wiederhergestellter Deal taucht danach wieder normal auf.
POST/deals
curl -X POST https://cockpit.set2sell.io/api/v1/deals \
-H "Authorization: Bearer s2s_live_…" \
-H "Content-Type: application/json" \
-d '{
"pipeline_id": "8a1e…",
"first_name": "Maria",
"last_name": "Beispiel",
"email": "maria@example.com",
"phone": "+49 170 1234567",
"company": "Beispiel GmbH",
"value": 2500,
"source": "dein-produkt",
"tags": ["webinar-2026-09"],
"notes": "Hat sich über das Herbst-Webinar angemeldet."
}'
Body-Felder:
| Feld | Typ | Pflicht | Hinweis |
|---|---|---|---|
pipeline_id |
UUID | ja | muss zum Workspace gehören, sonst 404 |
stage_id |
UUID | nein | Standard: erste Phase der Pipeline; muss zur Pipeline gehören, sonst 422 |
title |
Text ≤ 255 | nein | Standard: contact_name, sonst E-Mail, sonst „API Lead" |
first_name, last_name |
Text ≤ 255 | nein | empfohlen statt contact_name |
contact_name |
Text ≤ 255 | nein | wird serverseitig am letzten Leerzeichen in Vor-/Nachname geteilt, wenn first_name/last_name fehlen |
email |
E-Mail ≤ 255 | nein | wird kleingeschrieben gespeichert |
phone |
Text ≤ 50 | nein | |
company |
Text ≤ 255 | nein | |
value |
Zahl 0 … 9.999.999.999 | nein | Deal-Wert in der Workspace-Währung |
notes |
Text ≤ 10.000 | nein | |
source |
Text ≤ 100 | nein | Standard api; setze hier den Namen deines Produkts |
tags |
Liste, max. 50 × 100 Zeichen | nein | |
priority |
low|medium|high|urgent |
nein | |
expected_close_date |
YYYY-MM-DD |
nein | |
setter_id, closer_id |
Nutzer-ID aus GET /members |
nein | muss ein aktives Mitglied des Workspace sein, sonst 422 mit Feldname |
Antwort: 201 mit { "data": Deal } (Deal-Objekt siehe 3.4). Beim Anlegen wird eine
Notiz „Deal über die öffentliche API erstellt" in die Zeitleiste des Deals geschrieben
und das Webhook-Ereignis deal.created ausgelöst. Automationen des Kunden mit dem
Auslöser „Deal: Neu erstellt" starten für API-Deals derzeit nicht (Matrix in 2.4).
Idempotenz: Mit dem Header Idempotency-Key (siehe 3.2) legt ein wiederholter
POST (etwa durch einen Netzwerk-Retry) keinen zweiten Deal an — du bekommst die
gespeicherte Antwort zurück. Ohne den Header gibt es keine serverseitige
Dublettenprüfung: Prüfe vor dem Anlegen per GET /deals?search=<email> oder
dedupliziere selbst. (Der Lead-Import-Webhook in Abschnitt 5 hat zusätzlich eine
inhaltliche Dublettenerkennung — das ist der funktionale Unterschied zwischen den Wegen.)
GET/deals/{id}
200 mit { "data": Deal }; 404, wenn der Deal nicht zum Workspace gehört oder
gelöscht ist; 422, wenn id keine UUID ist.
PATCH/deals/{id}
Akzeptiert dieselben Felder wie POST (pipeline_id nur zusammen mit stage_id, siehe unten), zusätzlich probability
(ganze Zahl 0–100) sowie expected_close_date, setter_id und closer_id mit null
zum Leeren. Nur gesendete Felder werden geändert.
curl -X PATCH https://cockpit.set2sell.io/api/v1/deals/9b7f… \
-H "Authorization: Bearer s2s_live_…" \
-H "Content-Type: application/json" \
-d '{ "stage_id": "c0d3…" }'
Ein stage_id-Wechsel setzt Status, Gewinn-/Abschlussdatum und Wahrscheinlichkeit
automatisch — und leert sie wieder: Ein Deal, der aus „gewonnen" nach „verloren" oder
in eine offene Phase zurückgeht, verliert sein won_date (bei offenen Phasen auch
closed_at). Lies Umsatz deshalb über status, won_date ist nur bei
status = gewonnen gesetzt. Ausgelöste Webhook-Ereignisse: immer deal.updated; bei Phasenwechsel
zusätzlich deal.stage_changed; beim Kippen des Status zusätzlich deal.won oder
deal.lost. Automationen des Kunden („Deal: Stage gewechselt", „Deal: Status geändert", „Deal: Tag hinzugefügt")
reagieren auf API-Änderungen derzeit nicht (Matrix in 2.4).
Pipeline wechseln: pipeline_id zusammen mit stage_id verschiebt den Deal in eine
andere Pipeline; die Phase muss zur Ziel-Pipeline gehören. Wie in der App entsteht ein
Zeitleisten-Eintrag (im Phasenverlauf als Wechsel mit unterschiedlichen
from_pipeline_id/to_pipeline_id). Archivierte oder gelöschte Ziel-Pipelines → 404,
pipeline_id ohne stage_id → 422.
Was PATCH bewusst nicht kann:
status,won_date,closed_atsetzen. Diese Felder sind abgeleitet.probabilitygegen eine Gewonnen-/Verloren-Phase durchsetzen: Wird beides in einer Anfrage geschickt, gewinnt die Ableitung (100 bzw. 0).
Eigene Felder schreiben: custom_fields ist ein Objekt mit dem Feldnamen als
Schlüssel (GET /custom-fields liefert Namen und Typen der Pipeline). Gültige Werte
werden gespeichert und mit 200 bestätigt. Nur ein unbekannter Name oder ein
falscher Typ antwortet 422, die Meldung nennt dann die vorhandenen Felder.
Mitgeschickte Felder werden gesetzt, alle anderen bleiben unverändert.
curl -X PATCH https://cockpit.set2sell.io/api/v1/deals/9b7f… \
-H "Authorization: Bearer s2s_live_…" \
-H "Content-Type: application/json" \
-d '{ "custom_fields": { "Wunsch-Coaching": "Einzel", "Budget": 2500 } }'
DELETE/deals/{id}
204 ohne Rumpf. Der Deal verschwindet sofort aus Oberfläche und API, bleibt aber
30 Tage wiederherstellbar (Soft-Delete). Laufende E-Mail-Sequenzen des Deals werden
beendet, und deal.deleted wird ausgelöst. 404, wenn der Deal nicht zum Workspace
gehört oder bereits gelöscht ist.
GET/deals/{id}/stage-history
Alle Phasenwechsel eines Deals, älteste zuerst — auch für bereits gelöschte Deals. Quelle ist die Zeitleiste des Deals; jeder Schreibweg (Board, API, Automation, MCP, Import, Buchung, Funnel) hinterlässt dort einen Eintrag, deshalb ist der Verlauf vollständig und reicht zeitlich vor die Einrichtung deines Webhook-Abos zurück.
{
"data": [
{
"id": "7c1a…", "deal_id": "9b7f…",
"from_stage_id": "c0d1…", "to_stage_id": "c0d2…",
"from_pipeline_id": "8a1e…", "to_pipeline_id": "8a1e…",
"from_status": "offen", "to_status": "termin",
"changed_at": "2026-09-12T14:03:11.412Z",
"changed_by": "user_3JB…", "actor_kind": "user"
}
]
}
changed_by ist die Nutzer-ID des Handelnden (GET /members); bei Automationen der
Deal-Besitzer, erkennbar an actor_kind: "automation". actor_kind ist null bei
Einträgen vor September 2026. Ein Pipeline-Wechsel erscheint als Wechsel mit
unterschiedlichen from_pipeline_id/to_pipeline_id.
POST/deals/{id}/notes
Notiz an die Zeitleiste des Deals — erscheint in der Oberfläche wie eine von Hand
geschriebene Notiz und löst note.created aus.
curl -X POST https://cockpit.set2sell.io/api/v1/deals/9b7f…/notes \
-H "Authorization: Bearer s2s_live_…" -H "Content-Type: application/json" \
-d '{ "text": "Rückruf für Donnerstag vereinbart." }'
text: 1 bis 10.000 Zeichen. Antwort 201 mit dem Eintrag im Format von GET /activities
(type: "note", user_id = Ersteller des API-Keys). 404 für fremde oder gelöschte Deals.
POST/deals/merge
Dubletten in einen Hauptdeal zusammenführen — dieselbe Funktion wie „Zusammenführen" in der Oberfläche.
curl -X POST https://cockpit.set2sell.io/api/v1/deals/merge \
-H "Authorization: Bearer s2s_live_…" -H "Content-Type: application/json" \
-d '{ "primary_deal_id": "9b7f…", "duplicate_deal_ids": ["1c2d…", "3e4f…"] }'
Was passiert: Leere Felder des Hauptdeals werden aus den Dubletten gefüllt (Kontakt,
Firma, Vor-/Nachname; beim Wert gewinnt der höhere), Tags werden vereinigt, Notizen
angehängt; Aktivitäten, Termine, Aufgaben, Anrufe und eigene Felder wandern zum
Hauptdeal; die Dubletten werden gelöscht (Soft-Delete). Ausgelöste Ereignisse:
deal.deleted je Dublette, dann deal.updated für den Hauptdeal.
{
"data": {
"deal": { …Hauptdeal… },
"merged_deal_ids": ["1c2d…", "3e4f…"],
"merged_count": 2,
"fields_updated": { "company": "Beispiel GmbH" }
}
}
Bis zu 20 Dubletten je Aufruf. 404, wenn einer der Deals nicht (mehr) zum Workspace
gehört — dann wird nichts zusammengeführt.
GET/stage-changes
Dieselben Einträge für den ganzen Workspace, chronologisch (changed_at, dann
id). Damit liest du den Verlauf einmal komplett und danach inkrementell.
| Parameter | Typ | Bedeutung |
|---|---|---|
since |
ISO-8601 mit Zeitzone | nur Wechsel mit changed_at >= Wert |
deal_id |
UUID | nur Wechsel dieses Deals |
limit |
1–100, Standard 50 | Seitengröße |
cursor |
UUID | next_cursor der vorigen Antwort (Eintrags-ID) |
{ "data": [ …Eintrag wie oben… ], "next_cursor": "7c1a…" }
Inkrementell: since auf den changed_at-Wert des letzten verarbeiteten Eintrags
setzen und über die Eintrags-id deduplizieren (der erste Treffer ist der bereits
bekannte). Innerhalb einer Antwort blätterst du mit cursor, bis next_cursor null
ist. Ein unbekannter cursor (fremder Workspace) antwortet 422.
GET/activities
Zeitleisten-Einträge des ganzen Workspace, chronologisch (created_at, dann id):
Anrufe, Notizen, E-Mails, Termine, Aufgaben, SMS, WhatsApp, Phasenwechsel. Das ist der
vollständige Kontaktverlauf je Interessent und die Aktivität je Teammitglied.
| Parameter | Typ | Bedeutung |
|---|---|---|
since |
ISO-8601 mit Zeitzone | nur Einträge mit created_at >= Wert |
deal_id |
UUID | nur Einträge dieses Deals |
type |
note|email|call|meeting|task|sms|whatsapp|stage_change|value_change|system |
nur dieser Typ |
user_id |
Nutzer-ID aus GET /members |
nur Einträge dieser Person |
limit |
1–100, Standard 50 | Seitengröße |
cursor |
UUID | next_cursor der vorigen Antwort |
{
"data": [
{
"id": "5e6f…", "deal_id": "9b7f…", "type": "call",
"description": "Anruf: erreicht, Termin vereinbart",
"user_id": "user_3JB…", "direction": "outbound", "duration_seconds": 312,
"created_at": "2026-09-12T14:03:11.412Z"
}
],
"next_cursor": null
}
direction (inbound | outbound) und duration_seconds sind bei Anrufen, SMS,
WhatsApp und E-Mails gesetzt, sonst null. user_id ist null bei Einträgen, die das
System geschrieben hat. Inkrementell wie bei /stage-changes: since auf den letzten
created_at setzen und über id deduplizieren.
GET/appointments
Termine des Workspace, sortiert nach Beginn (start_time, dann id).
| Parameter | Typ | Bedeutung |
|---|---|---|
updated_since |
ISO-8601 mit Zeitzone | nur Termine mit updated_at >= Wert (Abgleich) |
start_after, start_before |
ISO-8601 mit Zeitzone | Zeitfenster auf start_time |
status |
neu|bestaetigt|erschienen|nicht_erschienen|abgesagt|verschoben |
nach Status filtern |
deal_id |
UUID | nur Termine dieses Deals |
host_id |
Nutzer-ID aus GET /members |
nur Termine dieses Gastgebers (Kalenderinhaber) |
event_type_id |
UUID | nur Termine dieser Terminart |
limit |
1–100, Standard 50 | Seitengröße |
order |
asc|desc, Standard asc |
Sortierung nach start_time |
cursor |
Text | next_cursor der vorigen Antwort, unverändert und mit demselben order (undurchsichtig, seit 29.10.2026 — siehe „Änderung am Vertrag“ unter „Termine buchen“) |
{
"data": [
{
"id": "7a8b…", "title": "Erstgespräch mit Maria Beispiel", "status": "erschienen",
"start_time": "2026-09-18T09:00:00.000Z", "end_time": "2026-09-18T09:30:00.000Z",
"deal_id": "9b7f…", "host_id": "user_3JB…", "created_by": "user_3JB…",
"event_type_id": "e1f2…", "event_type_name": "Erstgespräch 30 min",
"location": "https://meet.example/…", "description": null,
"booking_source": "booking_page", "timezone": "Europe/Berlin",
"created_at": "…", "updated_at": "…"
}
],
"next_cursor": null
}
host_id ist der Gastgeber (Kalenderinhaber, meist der Closer), created_by wer den
Termin angelegt hat (bei Setter-Buchungen der Setter; bei Buchungen über Buchungsseite
oder Funnel ebenfalls der Gastgeber). Status-Bedeutung: erschienen = stattgefunden,
nicht_erschienen = No-Show, abgesagt = abgesagt, verschoben = neu terminiert.
GET/custom-fields
Optionaler Filter ?pipeline_id=<uuid> (404, wenn die Pipeline nicht zum Workspace
gehört). Ohne Filter: Felder aller aktiven Pipelines.
{
"data": [
{
"id": "f1a2…", "pipeline_id": "8a1e…",
"name": "Wunsch-Coaching", "type": "select",
"required": false,
"options": { "options": ["Einzel", "Gruppe"] },
"description": null
}
]
}
name ist zugleich der Schlüssel, unter dem der Wert im Deal-Objekt
(custom_fields) und im Lead-Import-Payload steht — exakte Schreibweise inklusive
Groß-/Kleinschreibung und Leerzeichen. type ist eines von
text | number | select | multiselect | boolean | date | email | phone | url.
🚨
optionshat zwei Formen — bitte beide behandeln. Das Feld wird so ausgeliefert, wie es gespeichert ist. Der Normalfall ist das Objekt{ "options": ["Einzel", "Gruppe"] }; aus Altbeständen kann dieselbe Angabe als flache Liste["Einzel", "Gruppe"]kommen. Bei Feldern ohne Auswahl stehtnulloder{}. Robust auslesen:const werte = Array.isArray(f.options) ? f.options : (f.options?.options ?? [])Achtung, Asymmetrie: Beim Anlegen und Ändern schicken Sie
optionsals flache Liste (siehePOST/PATCHunten) — zurück kommt sie als Objekt. Falls wir die Ausgabe künftig vereinheitlichen, kündigen wir das vorher an; mit dem Schnipsel oben sind Sie in beiden Fällen auf der sicheren Seite.
POST/custom-fields
Eigenes Feld an einer Pipeline anlegen — etwa beim Verbinden, statt das Team darum zu
bitten. Gleicher Schreibpfad wie das MCP-Werkzeug create_custom_field.
curl -X POST https://cockpit.set2sell.io/api/v1/custom-fields \
-H "Authorization: Bearer s2s_live_…" -H "Content-Type: application/json" \
-d '{ "pipeline_id": "8a1e…", "name": "Partner-Code", "type": "text" }'
| Feld | Typ | Pflicht | Hinweis |
|---|---|---|---|
pipeline_id |
UUID | ja | eigene Felder hängen an einer Pipeline |
name |
Text ≤ 255 | ja | Anzeigename, zugleich der Schlüssel in custom_fields; eindeutig je Pipeline |
type |
siehe 11.3 | ja | |
options |
Liste, max. 100 × 255 Zeichen | bei select/multiselect |
Auswahlwerte |
required |
boolean | nein | Standard false |
description |
Text ≤ 1.000 | nein |
Antwort 201 im Format von GET /custom-fields. 409 conflict, wenn der Name in der
Pipeline schon vergeben ist; 404, wenn die Pipeline nicht zum Workspace gehört.
PATCH/custom-fields/{id}·DELETE/custom-fields/{id}
Ein bestehendes Feld nachziehen — etwa wenn sich die Beschreibung in Ihrer Oberfläche
ändert. Die id stammt aus GET /custom-fields oder aus der Antwort von POST.
curl -X PATCH https://cockpit.set2sell.io/api/v1/custom-fields/f1a2… \
-H "Authorization: Bearer s2s_live_…" -H "Content-Type: application/json" \
-d '{ "description": "Modul 3 · Stand September" }'
| Feld | Typ | Hinweis |
|---|---|---|
description |
Text ≤ 1.000 oder null |
null leert die Beschreibung |
required |
boolean | |
options |
Liste, 1–100 × 255 Zeichen | nur bei select/multiselect; ersetzt die Liste vollständig |
Mindestens ein Feld ist Pflicht. Antwort 200 im Format von GET /custom-fields.
name und type sind nicht änderbar (422). Der Name ist der Schlüssel, unter dem
die Werte am Deal stehen (custom_fields), und auch der Funnel-Weg ordnet eingehende
Antworten über den Namen zu — ein umbenanntes Feld würde er beim nächsten Eingang als
neues Feld anlegen, statt den Wert dem bestehenden zuzuordnen. Wer einen anderen Namen
braucht, legt ein neues Feld an.
DELETE entfernt die Felddefinition und antwortet mit { "ok": true }. Dabei gehen die
gespeicherten Werte dieses Feldes an allen Deals verloren — das lässt sich nicht
rückgängig machen. Beide Aufrufe antworten mit 404, wenn das Feld nicht existiert oder
nicht zum Workspace des Schlüssels gehört.
POST/hooks·PATCH/hooks/{id}·DELETE/hooks/{id}
Damit legt eine Integration ein Webhook-Abo programmatisch an, ohne dass der Kunde die Oberfläche bedienen muss (so arbeitet unsere Zapier-App).
curl -X POST https://cockpit.set2sell.io/api/v1/hooks \
-H "Authorization: Bearer s2s_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Dein Produkt",
"url": "https://hooks.dein-produkt.de/set2sell/abc123",
"events": ["deal.created", "deal.stage_changed", "appointment.created"]
}'
201 mit { "data": { "id": "…", "secret": "…" } }. Das Secret wird nur in dieser
Antwort ausgegeben — sofort speichern, du brauchst es für die Signaturprüfung.
422, wenn die URL kein HTTPS ist oder auf eine private Adresse zeigt. name ist
optional (Standard „Zapier"). Erlaubte events siehe Abschnitt 4.2.
Per API angelegte Abos erscheinen dem Kunden in der Oberfläche als schreibgeschützt („von Zapier verwaltet"): Er sieht sie samt Zustellprotokoll, kann sie aber nicht bearbeiten. Anlegen und Entfernen liegt bei deiner Integration.
DELETE /hooks/{id} → 200 { "ok": true }. 404, wenn die ID unbekannt ist oder
das Abo vom Kunden in der Oberfläche angelegt wurde — solche Abos sind über die API
unantastbar.
Abo ändern: PATCH /hooks/{id} mit url, events, name und/oder active
(alle optional, nur gesendete Felder ändern sich). Das Secret bleibt bestehen — Löschen
und Neuanlage sind bei einer neuen Adresse nicht mehr nötig. active: true hebt eine
Auto-Pause auf und setzt den Fehlerzähler zurück. Wie DELETE wirkt PATCH nur auf per
API angelegte Abos; in der Oberfläche angelegte antworten 404.
GET/webhook-subscriptions
Alle Abos des Workspace, egal ob per Oberfläche oder API angelegt — ohne Secret.
{
"data": [
{
"id": "…", "name": "Dein Produkt", "url": "https://…",
"events": ["deal.stage_changed"], "active": true,
"paused_at": null, "paused_reason": null,
"failure_count": 0, "created_at": "2026-09-01T10:00:00.000Z"
}
]
}
failure_count zählt endgültig gescheiterte Zustellungen in Folge; jede erfolgreiche
Zustellung setzt ihn auf 0. Ab 20 wird das Abo automatisch pausiert (active: false,
paused_at und paused_reason gesetzt). Damit kann deine Integration selbst erkennen,
dass sie nichts mehr bekommt, statt auf den Kunden zu warten.
GET/webhook-subscriptions/{id}/deliveries
Zustellprotokoll eines Abos, neueste zuerst. Query: limit (1–100, Standard 50),
status (pending | delivering | succeeded | failed | dead), since (ISO-8601,
created_at >= since).
{
"data": [
{
"id": "…", "event": "deal.stage_changed", "status": "succeeded",
"attempt": 1, "last_response_status": 200, "last_response_body": "ok",
"last_error": null, "duration_ms": 184,
"created_at": "…", "delivered_at": "…", "next_attempt_at": null,
"payload": { "id": "evt_…", "type": "deal.stage_changed", "data": { "…": "…" } }
}
]
}
payload ist der gesendete Umschlag (Form in 4.3) — damit lässt sich eine
fehlgeschlagene Zustellung ohne Nachstellen prüfen.
Bewusst ohne Cursor: Zustellungen eines Ereignis-Stapels teilen exakt denselben
created_at-Stempel. Inkrementell pollst du mit since (leicht überlappend) und
dedupliziert über die Delivery-id.
3.4 Das Deal-Objekt
So sieht ein Deal in allen v1-Antworten aus:
{
"id": "9b7f…",
"title": "Maria Beispiel",
"status": "offen",
"pipeline_id": "8a1e…",
"stage_id": "c0d1…",
"value": 2500,
"contact": {
"name": "Maria Beispiel",
"first_name": "Maria",
"last_name": "Beispiel",
"email": "maria@example.com",
"phone": "+49 170 1234567",
"company": "Beispiel GmbH"
},
"tags": ["webinar-2026-09"],
"custom_fields": { "Wunsch-Coaching": "Einzel" },
"priority": "medium",
"probability": 50,
"source": "dein-produkt",
"notes": "Hat sich über das Herbst-Webinar angemeldet.",
"expected_close_date": null,
"won_date": null,
"closed_at": null,
"owner_id": "user_3JB…",
"setter_id": "user_3JB…",
"closer_id": null,
"stage_entered_at": "2026-09-12T14:03:11.412Z",
"last_touch_at": "2026-09-14T09:20:00.000Z",
"created_at": "2026-09-11T08:00:00.000Z",
"updated_at": "2026-09-11T08:00:00.000Z",
"deleted_at": null
}
Personen und Uhren:
| Feld | Bedeutung |
|---|---|
owner_id |
Zuständiger (Besitzer) des Deals — Nutzer-ID, auflösbar über GET /members |
setter_id, closer_id |
Setter und Closer, wie in der Oberfläche am Deal gepflegt; schreibbar über POST/PATCH |
stage_entered_at |
Eintritt in die aktuelle Phase (Verweildauer = jetzt − Wert) |
last_touch_at |
letzte menschliche Berührung: Anruf, Notiz, E-Mail, Termin, Aufgabe, SMS, WhatsApp, Phasenwechsel. Nicht jede Zeilenänderung — Automationen und Importe zählen nicht |
deleted_at |
nur belegt in Antworten auf ?deleted_since=…, sonst null |
custom_fields ist nach Feldname geschlüsselt und zeigt dieselben Werte wie die
Oberfläche. Fehlende Werte sind null, nicht weggelassen.
Achtung, zwei Formen: Die REST-API liefert Kontaktdaten verschachtelt
(contact.email). Webhook-Payloads liefern dagegen die flache Datenbankzeile
(contact_email). Eine Integration, die beides verarbeitet, muss beide Formen kennen.
Die Zuordnung steht in Abschnitt 4.3.
3.5 Verhaltensregeln auf einen Blick
- Status folgt der Phase. Nie
statussenden;stage_idverschieben. contact_namevs. Vor-/Nachname. Sende bevorzugtfirst_nameundlast_name. Ein alleinigercontact_namewird am letzten Leerzeichen geteilt („Anna Maria Müller" → Vorname „Anna Maria", Nachname „Müller"). Werden alle drei gesendet, gewinnen Vor- und Nachname.- Idempotenz per
Idempotency-Keybei jedemPOST(3.2) — für Retries immer mitsenden. Der Lead-Import-Webhook dedupliziert zusätzlich inhaltlich. - Cursor ist nicht chronologisch. Für Deltas
updated_since. - Löschen ist ein Soft-Delete. 30 Tage wiederherstellbar (durch den Kunden oder per
POST /deals/{id}/restore); nachvollziehbar überGET /deals?deleted_since=…. won_dategilt nur beistatus = gewonnen. Wandert der Deal weiter, wird es geleert. Umsatz immer überstatuslesen.- Personen sind Nutzer-IDs.
owner_id,setter_id,closer_id,changed_byüberGET /membersauflösen; die Zuordnung zu deinem System läuft überemail. sourcegehört dir. Setze deinen Produktnamen; der Kunde sieht ihn als Lead-Quelle und kann danach filtern.
3.6 Beispiel-Ablauf: Anbindung in vier Aufrufen
KEY="s2s_live_…"; BASE="https://cockpit.set2sell.io/api/v1"
# 1. Verbindung prüfen
curl -s "$BASE/me" -H "Authorization: Bearer $KEY"
# 2. Pipelines und Phasen holen, Ziel-Pipeline merken
curl -s "$BASE/pipelines" -H "Authorization: Bearer $KEY"
# 3. Deal anlegen
curl -s -X POST "$BASE/deals" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"pipeline_id":"8a1e…","first_name":"Maria","last_name":"Beispiel","email":"maria@example.com","source":"dein-produkt"}'
# 4. Später: Deal in die Phase „Termin" verschieben und den Setter eintragen
curl -s -X PATCH "$BASE/deals/9b7f…" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"stage_id":"c0d2…","setter_id":"user_3JB…"}'
# 5. Für Auswertungen: Mitglieder und Phasenverlauf lesen
curl -s "$BASE/members" -H "Authorization: Bearer $KEY"
curl -s "$BASE/stage-changes?since=2026-09-01T00:00:00Z" -H "Authorization: Bearer $KEY"
4. Ausgehende Webhooks (Ereignis-Abos)
Set2Sell schickt Ereignisse per HTTP POST an eine URL deiner Wahl, sobald sie
passieren. Die Zustellung ist signiert, wird bei Fehlern wiederholt und ist für den
Kunden und für dich (per API) nachvollziehbar.
4.1 Abo einrichten
Über die Oberfläche (durch den Kunden): Einstellungen → Workspace → Tab „Webhooks" → Neuer Webhook: Name, HTTPS-URL, Ereignisse auswählen, optional eigene Header (z. B. ein eigener Auth-Token) mitgeben. Nach dem Speichern wird das Signing-Secret einmalig angezeigt.
Per API (durch deine Integration): POST /api/v1/hooks, siehe Abschnitt 3.3.
Über die Oberfläche kann der Kunde außerdem jederzeit ein Testereignis
(webhook.test) auslösen, fehlgeschlagene Zustellungen erneut anstoßen und ein
pausiertes Abo reaktivieren.
4.2 Ereignisse
| Ereignis | Wann | Form von data |
|---|---|---|
deal.created |
Deal von Hand in der Oberfläche oder über POST /api/v1/deals angelegt |
Deal-Zeile |
deal.updated |
Deal geändert | Deal-Zeile |
deal.stage_changed |
Deal in andere Phase verschoben (Board, API, Automation, MCP, Import) | { deal, previous_stage_id, new_stage_id } |
deal.won |
Status kippt auf gewonnen |
Deal-Zeile |
deal.lost |
Status kippt auf verloren |
Deal-Zeile |
deal.deleted |
Deal gelöscht, auch als Dublette bei POST /deals/merge |
Deal-Zeile (Stand vor dem Löschen) |
appointment.created |
Termin gebucht (Buchungsseite, Funnel, Oberfläche, MCP) | Termin-Zeile + event_type_name, bei Buchungen zusätzlich attribution |
appointment.cancelled |
Termin abgesagt | Termin-Zeile |
appointment.rescheduled |
Start- oder Endzeit geändert | { appointment, previous_start_time, previous_end_time } |
task.created |
Aufgabe angelegt | Aufgaben-Zeile |
task.completed |
Aufgabe erledigt | Aufgaben-Zeile |
call.completed |
Telefonat über die Cockpit-Telefonie beendet | { id, team_id, call_sid, phone, direction, duration, result, user_id, deal_id, created_at } |
note.created |
Nutzer schreibt eine Notiz an einen Deal oder POST /api/v1/deals/{id}/notes (keine automatischen Aktivitäten) |
Aktivitäts-Zeile |
lead.imported |
Kunde importiert Deals per CSV/Datei oder Pipeline-Import in der Oberfläche | { import_id, count, deals[] } bzw. { source: "pipeline_import", pipeline_id, count } |
funnel.submission_completed |
Besucher schickt die letzte Formularseite eines Funnels ab (genau einmal pro Durchlauf; fields = alle Seiten) |
{ funnel_id, funnel_name, deal_id, fields, quiz_score } |
funnel.lead_captured |
Durchlauf hat einen Deal (E-Mail/Telefon bekannt), letzte Formularseite steht noch aus — für Abbrecher; bei einseitigen Funnels nie | { funnel_id, funnel_name, deal_id, fields, quiz_score } |
webhook.test |
Kunde klickt „Test-Event senden" | { message, triggered_by_user_id, timestamp } |
Hinweise:
deal.createddeckt nicht jeden Entstehungsweg ab (Matrix in 2.4). Es feuert, wenn ein Nutzer einen Deal in der Oberfläche anlegt oder deine IntegrationPOST /api/v1/dealsaufruft. Deals aus Funnel, Buchungsseite, Lead-Import-Webhook oder MCP lösen es nicht aus. Für diese Wege abonniere die Quell-Ereignisse:funnel.submission_completed(trägtdeal_id) undappointment.created(trägtdeal_idin der Termin-Zeile). Wenn dein Produkt selbst Leads einspielt und gleichzeitig auf neue Deals hören will, lege sie überPOST /api/v1/dealsan.lead.importedfeuert nur bei Datei- und Pipeline-Importen in der Oberfläche, nicht für Deals aus REST-API oder Lead-Import-Webhook.deal_idbeifunnel.submission_completedistnull, solange die Einreichung noch keinem Deal zugeordnet ist;quiz_scoreistnullaußerhalb von Quiz-Funnels.- Mehrseitige Funnels:
funnel.lead_capturedkommt nach der Seite, die den Deal entstehen lässt,funnel.submission_completederst nach der letzten Formularseite — beide mit derselbendeal_id. Wer nur vollständige Datensätze will, abonniert ausschließlichsubmission_completed. appointment.createdträgtattribution(Werbe-Klick-IDs, UTM-Parameter, Landing-URL), wenn die Buchung über eine Buchungsseite oder einen Funnel kam.
4.3 Zustellformat
Jede Zustellung ist ein POST mit Content-Type: application/json und diesen Headern:
X-Set2Sell-Event: deal.stage_changed
X-Set2Sell-Delivery-Id: 6d0c1c1e-… ← eindeutig pro Zustellung
X-Set2Sell-Timestamp: 1757577600 ← Unix-Sekunden
X-Set2Sell-Signature: sha256=3f9a… ← siehe 4.4
Dazu kommen die vom Kunden konfigurierten eigenen Header. Der Body ist ein Umschlag:
{
"id": "evt_6d0c1c1e-…",
"event": "deal.stage_changed",
"created_at": "2026-09-11T08:00:00.000Z",
"team_id": "3f2c…",
"data": {
"deal": {
"id": "9b7f…",
"title": "Maria Beispiel",
"status": "termin",
"pipeline_id": "8a1e…",
"stage_id": "c0d2…",
"value": 2500,
"contact_name": "Maria Beispiel",
"first_name": "Maria",
"last_name": "Beispiel",
"contact_email": "maria@example.com",
"contact_phone": "+49 170 1234567",
"company": "Beispiel GmbH",
"tags": ["webinar-2026-09"],
"custom_fields": { "Wunsch-Coaching": "Einzel" },
"source": "dein-produkt",
"user_id": "user_…",
"created_at": "…",
"updated_at": "…"
},
"previous_stage_id": "c0d1…",
"new_stage_id": "c0d2…"
}
}
id im Umschlag ist evt_ + Delivery-ID. Bei einem erneuten Zustellversuch bleiben
id, Body und Delivery-ID gleich; nur X-Set2Sell-Timestamp und die Signatur werden
neu berechnet.
Deal-Zeile (flach) ↔ REST-Deal (verschachtelt):
Webhook (data) |
REST-API |
|---|---|
contact_name |
contact.name |
first_name, last_name |
contact.first_name, contact.last_name |
contact_email |
contact.email |
contact_phone |
contact.phone |
company |
contact.company |
custom_fields (nach Feldname) |
custom_fields (identisch) |
user_id |
owner_id |
setter_id, closer_id, stage_entered_at, last_touch_at |
gleichnamig |
weitere Spalten der Datenbankzeile (probability, notes, …) |
Teilmenge davon |
Die Deal-Zeile ist die vollständige Datenbankzeile. Neue Spalten können jederzeit dazukommen; verlasse dich nur auf die hier genannten Felder und ignoriere unbekannte.
4.4 Signatur prüfen
Signatur = sha256= + Hex(HMAC-SHA256(Secret, <timestamp>.<raw-body>)).
- Raw Body heißt: die Bytes, wie sie ankommen — vor dem JSON-Parsen und ohne Neuformatierung.
- Vergleiche timing-sicher.
- Weise Zustellungen ab, deren Timestamp mehr als 5 Minuten von deiner Uhr abweicht (Replay-Schutz).
Node.js
const crypto = require('crypto')
function verify(secret, headers, rawBody) {
const ts = headers['x-set2sell-timestamp']
const sig = headers['x-set2sell-signature'] || ''
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
const expected = 'sha256=' + crypto.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`).digest('hex')
return expected.length === sig.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
}
Python
import hmac, hashlib, time
def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
ts = headers.get('X-Set2Sell-Timestamp', '')
sig = headers.get('X-Set2Sell-Signature', '')
if abs(time.time() - int(ts or 0)) > 300:
return False
expected = 'sha256=' + hmac.new(
secret.encode(), f'{ts}.'.encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, sig)
PHP
function verify(string $secret): bool {
$ts = $_SERVER['HTTP_X_SET2SELL_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_SET2SELL_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
if (abs(time() - intval($ts)) > 300) return false;
$expected = 'sha256=' . hash_hmac('sha256', "{$ts}.{$body}", $secret);
return hash_equals($expected, $sig);
}
4.5 Zustellung, Wiederholung, Auto-Pause
| Regel | Wert |
|---|---|
| Erfolg | jede Antwort mit Status 2xx |
| Timeout | 10 Sekunden pro Versuch |
| Versuche | bis zu 5 |
| Abstände nach Fehlschlag | 1 Min → 5 Min → 30 Min → 2 Std → 12 Std (insgesamt rund 14 Stunden) |
| nach dem 5. Fehlschlag | Zustellung dead; der Kunde kann sie in der Oberfläche erneut anstoßen |
| Auto-Pause | 20 dead-Zustellungen in Folge → Abo pausiert, Ersteller wird per E-Mail informiert |
| Reaktivierung | durch den Kunden in der Oberfläche; setzt den Zähler zurück |
| Versandtakt | Ausgehende Zustellungen werden im Minutentakt verschickt; rechne mit bis zu ~60 Sekunden Verzug |
| Reihenfolge | nicht garantiert; sortiere bei Bedarf nach created_at im Umschlag |
Gespeichert werden pro Zustellung Antwortstatus, die ersten 4.096 Zeichen des
Antwortkörpers, Antwort-Header, Fehlermeldung und Dauer — abrufbar für den Kunden in
der Oberfläche und für dich über GET /webhook-subscriptions/{id}/deliveries.
4.6 Empfehlungen für Empfänger
- Sofort
2xxantworten, dann verarbeiten. Alles, was länger als 10 Sekunden dauert, gilt als Fehlschlag und wird wiederholt. - Idempotent verarbeiten. Dedupliziere über
X-Set2Sell-Delivery-Id(oderidim Umschlag); Wiederholungen tragen dieselbe ID. - Signatur und Timestamp prüfen, nicht die Absender-IP.
- Unbekannte Felder ignorieren. Payloads wachsen additiv.
- Pausen überwachen: Poll
GET /webhook-subscriptionsgelegentlich oder bitte den Kunden, die Pause-Mail zu beachten.
4.7 Zielsystem-Format „Trakyo"
Neben dem Standard-Umschlag gibt es ein festes Sonderformat für die
YouTube-Attributions-Plattform Trakyo: flacher Body (trakyo_id, email, name,
phone, event_name) nur für appointment.created aus Buchungsseiten und Funnels,
ohne Umschlag und ohne Signatur. Das wählt der Kunde beim Anlegen des Abos als
„Zielsystem". Andere Partner-spezifische Formate sind auf Anfrage möglich; Standard ist
der Umschlag aus 4.3.
5. Eingehender Lead-Import-Webhook
Der Lead-Import-Webhook ist der ältere, auf No-Code-Tools zugeschnittene Weg, Leads
einzuspielen. Sein Mehrwert gegenüber POST /api/v1/deals: Dublettenerkennung mit
Anreicherung, Pipeline und Phase per Name statt UUID, Schreiben von eigenen
Feldern und ein Batch-Modus.
Dafür ist die Authentifizierung schwächer (Nutzer-ID statt Secret), das Fehlerformat
ein anderes, und aus per Webhook angelegten Deals feuert kein deal.created-Abo.
Für neue, produktive Partner-Integrationen empfehlen wir deshalb die REST-API v1;
dieser Abschnitt gilt vor allem für Zapier-Catch-Hooks, Make, n8n und Formular-Tools.
5.1 Endpunkt und Header
POST https://cockpit.set2sell.io/api/webhooks/lead-import
Content-Type: application/json
x-user-id: user_…
x-user-idist die Nutzer-ID eines Mitglieds des Ziel-Workspace (beginnt mituser_). Der Kunde findet sie unter Einstellungen → Webhooks zusammen mit einem Konfigurations-Generator, der die fertige Zapier-/Make-Konfiguration erzeugt. Fehlt der Header:401; falsches Format:403.x-webhook-signature(HMAC-SHA256 über den Rohbody, Hex, optional mit Präfixsha256=) ist vorgesehen, aber für Partner nicht erforderlich.- Rate-Limit: 50 Anfragen pro Minute und Nutzer-ID (ohne Header: pro IP).
Bei Überschreitung
429mitretryAfterin Sekunden.
Ein GET auf denselben Pfad liefert eine maschinenlesbare Selbstbeschreibung
(Felder, Formate); mit ?options=true und x-user-id zusätzlich die Pipelines und
Phasen des Nutzers samt IDs.
5.2 Einzel-Lead
{
"title": "Maria Beispiel",
"pipeline": "Vertrieb",
"stage": "Neu",
"first_name": "Maria",
"last_name": "Beispiel",
"contact_email": "maria@example.com",
"contact_phone": "+49 170 1234567",
"company": "Beispiel GmbH",
"value": 2500,
"notes": "Webinar-Anmeldung",
"source": "dein-produkt",
"source_id": "lead_48213",
"tags": ["webinar-2026-09"],
"custom_fields": { "Wunsch-Coaching": "Einzel" },
"mode": "enrich"
}
| Feld | Typ | Pflicht | Hinweis |
|---|---|---|---|
title |
Text | ja | Deal-Titel |
pipeline_id oder pipeline |
UUID / Name | nein* | Name wird ohne Groß-/Kleinschreibung unter den Pipelines gesucht, die dieser Nutzer angelegt hat. Für Pipelines anderer Mitglieder pipeline_id verwenden |
stage_id oder stage |
UUID / Name | nein | Standard: erste Phase |
first_name, last_name, contact_name |
Text | nein | wie in der REST-API; contact_name wird am ersten Leerzeichen geteilt |
contact_email |
nein | Achtung: hier contact_email, nicht email |
|
contact_phone, company |
Text | nein | |
value |
Zahl ≥ 0 | nein | Standard 0 |
probability |
0–100 | nein | Standard 0 |
expected_close_date |
Text (ISO-Datum) | nein | |
notes |
Text | nein | |
source, source_id |
Text | nein | eigene Herkunftskennung, source_id = ID in deinem System |
tags |
Liste von Text | nein | |
custom_fields |
Objekt, Schlüssel = Feldname | nein | wird gegen die Felddefinitionen der Pipeline geprüft (Pflichtfelder, Typen) |
mode |
enrich (Standard) / update |
nein | Verhalten bei Dubletten, siehe 5.4 |
* Fehlen pipeline_id und pipeline, wird die Standard-Pipeline des Nutzers verwendet.
Gibt es keine, schlägt der Aufruf mit einer Fehlermeldung fehl.
5.3 Batch (bis 100 Leads)
{
"default_pipeline_name": "Vertrieb",
"default_stage_name": "Neu",
"leads": [
{ "title": "Lead 1", "contact_email": "a@example.com" },
{ "title": "Lead 2", "contact_email": "b@example.com", "stage": "Termin" }
]
}
Statt der Namen gehen auch default_pipeline_id / default_stage_id. Jeder Lead darf
die Vorgaben mit eigenen pipeline/stage-Angaben überschreiben. Jeder Lead wird
einzeln verarbeitet; ein fehlerhafter Lead bricht den Stapel nicht ab.
5.4 Dublettenerkennung und Anreicherung
Vor dem Anlegen wird innerhalb des Workspace nach einem bestehenden Deal gesucht, in dieser Reihenfolge:
- gleiche E-Mail (normalisiert),
- sonst gleiche Telefonnummer (normalisiert),
- sonst gleiche Firma + Kontaktname.
Wird ein Treffer gefunden, wird kein neuer Deal angelegt, sondern der bestehende bearbeitet:
mode |
Felder | Phase |
|---|---|---|
enrich (Standard) |
füllt nur leere Felder des bestehenden Deals; custom_fields ergänzt nur neue Schlüssel; tags werden vereinigt; notes werden mit Datum angehängt |
bleibt unverändert |
update |
überschreibt gesendete, nicht-leere Felder | wird in die ausdrücklich angegebene Phase verschoben — nur vorwärts und nur innerhalb derselben Pipeline; dabei feuern deal.stage_changed und ggf. deal.won/deal.lost |
Die Antwort sagt dir, was passiert ist:
{
"success": true,
"action": "enriched",
"deal_id": "9b7f…",
"message": "Existing deal enriched with 2 new field(s): contact_phone, company",
"fields_updated": ["contact_phone", "company"]
}
5.5 Antworten
| HTTP | Body | Bedeutung |
|---|---|---|
| 200 | { "success": true, "deal_id": "…", "message": "Lead imported successfully" } |
neuer Deal angelegt |
| 200 | { "success": true, "action": "enriched", "deal_id": "…", "fields_updated": [...] } |
Dublette gefunden, bestehender Deal bearbeitet |
| 200 | { "success": true, "message": "Processed 3 leads", "results": [...], "summary": { "total", "successful", "failed" } } |
Batch verarbeitet; results[i] trägt success, deal_id bzw. error je Lead |
| 400 | { "success": false, "error": "Invalid payload format", "details": [...] } |
Schema verletzt (z. B. title fehlt, ungültige UUID) |
| 400 | { "success": false, "error": "Custom field validation failed", "details": [...] } |
Pflichtfeld fehlt oder Typ passt nicht |
| 401 | { "success": false, "error": "User authentication required…" } |
x-user-id fehlt |
| 403 | { "success": false, "error": "Invalid user ID format" } |
x-user-id beginnt nicht mit user_ |
| 429 | { "success": false, "error": "Webhook rate limit exceeded…", "retryAfter": 30 } |
Limit erreicht |
| 500 | { "success": false, "error": "…", "hint": "…" } |
z. B. keine Pipeline gefunden; die Meldung nennt die Ursache |
5.6 Was der Lead-Import auslöst und was nicht
- Angelegte Deals starten Automationen des Kunden mit Auslöser „Deal: Neu erstellt" — der einzige Integrationsweg, der das heute tut (Matrix in 2.4).
- Angelegte Deals lösen kein Webhook-Abo
deal.createdund keinlead.importedaus. - Im
update-Modus lösen Phasenwechsel die Webhook-Abosdeal.stage_changedsowiedeal.won/deal.lostaus, aber keine Automation „Deal: Stage gewechselt". - Jeder Aufruf wird serverseitig protokolliert (Status, Dauer, Fehler; Signaturen und Auth-Header geschwärzt). Bei Support-Anfragen hilft deshalb der Zeitstempel des Aufrufs.
6. Zapier, Make und n8n
6.1 Zapier-App „Set2Sell Cockpit"
Es gibt eine fertige Zapier-App. Sie ist als private App veröffentlicht, das heißt Kunden erreichen sie über einen Einladungslink, nicht über die Zapier-Suche:
https://zapier.com/developer/public-invite/244466/98564210ea8523c8d3d4a36cb30a2a4a/
Der Kunde findet denselben Link mit einer Schritt-für-Schritt-Anleitung im Cockpit unter Einstellungen → Workspace → Tab „API".
Verbindung: API-Key (s2s_live_…). Die App ruft GET /me als Verbindungstest auf
und zeigt den Workspace-Namen als Verbindungsbezeichnung.
Trigger (alle instant, per Webhook-Abo):
| Trigger | Ereignis | Payload |
|---|---|---|
| Neuer Lead | lead.imported |
Deal-Felder flach (nur bei Datei-/Pipeline-Import in der Oberfläche) |
| Deal gewonnen | deal.won |
Deal-Felder flach |
| Deal verloren | deal.lost |
Deal-Felder flach |
| Deal-Phase geändert | deal.stage_changed |
deal, previous_stage_id, new_stage_id |
| Termin gebucht | appointment.created |
Termin-Felder flach |
| Funnel-Formular abgeschickt | funnel.submission_completed |
funnel_id, funnel_name, deal_id, fields (alle Seiten), quiz_score — genau einmal pro Durchlauf, nach der letzten Formularseite |
| Funnel-Lead erfasst (Formular noch nicht abgeschlossen) | funnel.lead_captured |
wie oben, fields = bisher ausgefüllte Felder — für Abbrecher |
Jeder Zap-Datensatz trägt zusätzlich id (Delivery-ID) und event. Die eigenen
Felder des Kunden erscheinen bei den Deal-Triggern als custom_fields__<Feldname>
(bei „Deal-Phase geändert" als deal__custom_fields__<Feldname>) — im Zap-Editor
beschriftet mit dem Feldnamen, auch wenn der Beispiel-Deal keinen Wert trägt.
Aktionen:
| Aktion | Ruft auf | Felder |
|---|---|---|
| Deal/Lead anlegen | POST /api/v1/deals |
Pipeline und Phase als Dropdown (dynamisch geladen), Titel, Kontaktname oder Vor-/Nachname, E-Mail, Telefon, Firma, Wert, Notizen, Quelle — plus die eigenen Felder der gewählten Pipeline (erscheinen nach der Pipeline-Auswahl; Pflichtfelder sind markiert, Auswahlfelder als Dropdown) |
| Deal aktualisieren | PATCH /api/v1/deals/{id} |
Deal-ID plus dieselben Felder außer Pipeline; eigene Felder über das optionale Dropdown „Pipeline (nur zum Laden der eigenen Felder)" — ohne Auswahl erscheinen die Felder aller Pipelines als „Pipeline → Feld". Die Pipeline des Deals bleibt unverändert. |
Suche:
| Suche | Ruft auf | Felder |
|---|---|---|
| Deal abrufen | GET /api/v1/deals/{id} |
Deal-ID (z. B. deal_id aus einem Funnel-Trigger). Liefert den Deal flach wie die Deal-Trigger: Standardfelder, Adresse (street, postal_code, city, country) und custom_fields. Reines Lesen — updated_at bleibt unberührt (kein leeres „Deal aktualisieren" mehr nötig). |
Die Abos, die Zapier anlegt, erscheinen dem Kunden im Webhook-Tab als „von Zapier verwaltet" und werden beim Ausschalten des Zaps automatisch wieder entfernt.
Beide Aktionen laufen über die REST-API. Damit gilt auch hier: Ein per Zap angelegter oder verschobener Deal startet derzeit keine Automation des Kunden (Matrix in 2.4). Wer das braucht, nutzt für das Anlegen „Webhooks by Zapier → POST" auf den Lead-Import-Webhook (Abschnitt 5).
Lücke, die du kennen solltest: Es gibt in der Zapier-App keinen Trigger für
deal.created. Wer auf jeden neuen Deal reagieren will, legt im Cockpit ein
Webhook-Abo auf deal.created an und nutzt in Zapier „Webhooks by Zapier → Catch Hook".
6.2 Make (ehemals Integromat) und n8n
Für beide gibt es keine eigene App. Sie arbeiten über ihre generischen Module:
- Leads anlegen / Deals lesen: HTTP-Modul gegen
https://cockpit.set2sell.io/api/v1/…mit HeaderAuthorization: Bearer s2s_live_…(Abschnitt 3). - Auf Ereignisse reagieren: „Custom Webhook" (Make) bzw. „Webhook"-Node (n8n) als Ziel-URL eines Webhook-Abos eintragen (Abschnitt 4). Die Signaturprüfung ist dort optional; das Secret kann aber als zweiter Faktor über einen eigenen Header mitgegeben werden (Kunde trägt beim Abo einen eigenen Header ein, das Szenario prüft ihn).
- Alternativ ohne API-Key: Lead-Import-Webhook (Abschnitt 5) mit
x-user-id.
7. KI-Anbindung per MCP
Set2Sell stellt einen MCP-Server (Model Context Protocol) bereit. Damit kann ein KI-Assistent des Kunden — etwa Claude im Web, als Desktop-App oder in Claude Code, aber auch jeder andere MCP-Client mit OAuth-Unterstützung — das CRM des Nutzers direkt bedienen: Deals suchen und anlegen, Termine buchen, Terminarten pflegen, Funnels bauen und veröffentlichen, Leads importieren.
7.1 Verbindung
| Connector-URL | https://cockpit.set2sell.io/api/mcp |
| Transport | Streamable HTTP (kein SSE) |
| Authentifizierung | OAuth 2.1 mit PKCE, Autorisierungsserver https://clerk.set2sell.io |
| Client-Registrierung | Dynamic Client Registration wird unterstützt; ein Client meldet sich selbst an, es braucht keine vorab hinterlegte Client-ID |
| Discovery | GET https://cockpit.set2sell.io/.well-known/oauth-protected-resource/mcp → nennt den Autorisierungsserver; dessen Metadaten liegen unter https://clerk.set2sell.io/.well-known/oauth-authorization-server |
| Rechte | pro Nutzer. Der Nutzer meldet sich mit seinem Cockpit-Konto an; jedes Werkzeug prüft bei jedem Aufruf die Workspace-Mitgliedschaft neu. Der Client sieht genau das, was der Nutzer sieht |
Ein Aufruf ohne Token liefert 401 mit WWW-Authenticate: Bearer … resource_metadata="…/.well-known/oauth-protected-resource/mcp" — MCP-Clients starten
daraus automatisch den Anmeldefluss. Der Kunde findet die Anleitung im Cockpit unter
Profil → Tab „KI-Anbindung (MCP)".
7.2 Werkzeuge (77)
| Bereich | Werkzeuge |
|---|---|
| Workspace | list_workspaces, list_workspace_members, invite_workspace_members, list_pending_invitations, revoke_invitation |
| Pipelines | list_pipelines, get_pipeline, get_call_script, list_pipeline_templates, create_pipeline, list_custom_fields, create_custom_field |
| Deals | search_deals, get_deal, create_deal, update_deal, move_deal, add_deal_note, add_deal_tags, remove_deal_tags, block_contact, list_deal_activities, list_deal_contacts, list_call_recordings, summarize_deal, import_leads, list_lead_conversations, document_call_result |
| Auswertung | get_deal_stats, get_pipeline_stats, query_workspace (nur lesend, über eine feste Tabellen-Auswahl) |
| Kontakte | search_contacts, get_contact, create_contact, update_contact, list_contact_fields |
| Termine | list_appointments, get_appointment, book_appointment, update_appointment, reschedule_appointment, cancel_appointment, delete_appointment, get_available_slots |
| Terminarten | list_event_types, get_event_type, create_event_type, update_event_type, duplicate_event_type, delete_event_type, set_event_type_members, set_event_type_availability, set_event_type_notifications |
| Verfügbarkeit & Kalender | get_availability, set_availability, list_availability_overrides, set_availability_override, delete_availability_override, list_calendar_connections, trigger_calendar_sync |
| Funnels | list_funnels, get_funnel, get_funnel_content, list_funnel_templates, create_funnel, create_funnel_from_template, update_funnel, publish_funnel, check_funnel_readiness, describe_funnel_capabilities, describe_workspace_knowledge, start_funnel_split_test, stop_funnel_split_test |
| Communities | list_communities, list_community_members, grant_community_access, revoke_community_access |
Jedes Werkzeug nimmt eine workspace_id entgegen; bei Mehrdeutigkeit ruft der Client
zuerst list_workspaces auf. Schreibende Werkzeuge protokollieren in der Zeitleiste des
Deals. Phasenwechsel über move_deal (oder update_deal mit stage_id) lösen
deal.stage_changed sowie deal.won / deal.lost aus — und wie ein Phasenwechsel im
Deal-Drawer die Zuweisungsregel der Zielphase, die Team-Benachrichtigung, phasengesteuerte
Sequenzen und Automationen (siehe 2.4); create_deal über MCP löst derzeit kein
deal.created aus (siehe Hinweise in 4.2).
7.3 Abgrenzung zur REST-API
| REST-API v1 | MCP | |
|---|---|---|
| Identität | Workspace (API-Key) | Nutzer (OAuth) |
| Typischer Nutzer | dein Backend | ein KI-Client des Kunden |
| Umfang | Deals (inkl. Notizen, Zusammenführen, Setter/Closer), Phasenverlauf, Aktivitäten, Termine, Mitglieder, Pipelines, Felder, Abos | 77 Werkzeuge quer durch das Produkt, u. a. Terminbuchung, Funnels, Community |
| Stabilität | versioniert (v1) |
Werkzeugliste wächst laufend; Clients sollten sie zur Laufzeit abfragen |
Wenn dein Produkt selbst ein KI-Agent ist, der im Namen eines Nutzers arbeitet, ist MCP der richtige Weg. Für Server-zu-Server-Integrationen bleibt es die REST-API.
8. Einbettungen
Diese Punkte betreffen weniger Partner-Backends als Website-Baukästen und Agenturen, die Cockpit-Elemente in Kundenseiten einbauen.
8.1 Buchungsseite
| Eigenständige Seite | https://cockpit.set2sell.io/b/<slug> |
| Einbett-Variante (ohne Rahmen) | https://cockpit.set2sell.io/b/<slug>/embed als <iframe> |
| Auto-Höhe | Die Einbett-Seite sendet postMessage({ type: "s2s.booking.height", height }) an die Elternseite; das iframe kann sich damit ohne inneren Scrollbalken anpassen |
| Eigene Domain | Der Kunde kann Buchungsseiten und Funnels unter eigener Domain betreiben (Einstellungen → Domains) |
Jede Buchung erzeugt einen Termin und, sofern noch keiner existiert, einen Deal, und
löst appointment.created aus.
8.2 Formulare
<script src="https://cockpit.set2sell.io/embed.js" data-form="<slug>" defer></script>
Das Script rendert das Formular an Ort und Stelle in einem iframe, passt die Höhe automatisch an und löst nach dem Absenden auf der Elternseite ein DOM-Ereignis aus:
document.addEventListener('set2sell-form:complete', (e) => {
console.log(e.detail.submission_id)
})
Optionale Attribute am Script-Tag: data-min-height="<px>" (Starthöhe, Standard 400)
und data-host="<url>", falls das Formular unter einer eigenen Domain läuft. Mehrere
Formulare auf einer Seite sind möglich.
Alternativ direkt als iframe: https://cockpit.set2sell.io/form/<slug>/embed.
8.3 Funnels
Veröffentlichte Funnels laufen unter https://cockpit.set2sell.io/f/<slug> oder der
eigenen Domain des Kunden. Formular-Abschlüsse lösen funnel.submission_completed,
Buchungen appointment.created aus. Beide tragen Attribution (UTM, fbclid, gclid,
Landing-URL), soweit vom Besucher mitgebracht.
9. Partner-Onboarding
9.1 So läuft eine Partnerschaft typischerweise
- Kontakt an
info@set2sell.iomit kurzer Beschreibung: Was macht euer Produkt, welche Richtung (Daten rein, Daten raus, beides), welche Ereignisse braucht ihr. - Test-Workspace. Wir legen einen Workspace für euch an, in dem ihr Admin seid: API-Keys, Webhook-Abos, Pipelines, Testdaten — alles selbst steuerbar.
- Bauen gegen die Produktionsumgebung mit dem Test-Workspace.
- Abnahme: Wir prüfen gemeinsam Signaturprüfung, Idempotenz, Fehlerbehandlung und Rate-Limit-Verhalten.
- Listung. Auf Wunsch nehmen wir euch in die Integrationsübersicht im Cockpit auf und stellen eine Kundenanleitung bereit.
9.2 Checkliste vor dem Go-live
REST-API
- Key wird verschlüsselt gespeichert und nie geloggt.
-
401führt zu einer klaren Meldung an den Kunden („Key widerrufen? Neuen Key eintragen"). -
429wird mitRetry-Afterrespektiert, nicht sofort wiederholt. -
422-Meldungen (error.message) werden dem Kunden oder eurem Support sichtbar gemacht. -
sourceist auf euren Produktnamen gesetzt. - Keine Duplikate durch Retries (eigene Deduplizierung oder
GET /deals?search=). -
statuswird nie gesendet; Phasenwechsel überstage_id.
Webhooks
- Signatur und Timestamp werden geprüft.
- Antwort
2xxinnerhalb weniger Sekunden, Verarbeitung asynchron. - Dedup über Delivery-ID.
- Unbekannte Felder werden ignoriert.
- Umgang mit pausierten Abos (Poll oder Kundenhinweis).
Lead-Import-Webhook (falls genutzt)
- Nutzer-ID wird als Konfigurationswert vom Kunden abgefragt, nicht erraten.
-
action: "enriched"wird korrekt als „Dublette, vorhandener Deal" behandelt. - Batch-
resultswerden je Lead ausgewertet.
9.3 Support
- Technische Fragen und Test-Workspace:
info@set2sell.io - Fehlerberichte bitte mit Zeitstempel (UTC), Workspace-ID (aus
GET /me), Delivery-ID (bei Webhooks) oder Request-Body (bei API-Fehlern, ohne Key).
10. Änderungspolitik
- Additive Änderungen ohne Ankündigung: neue Felder in Antworten und Payloads, neue Ereignisse, neue Endpunkte, neue MCP-Werkzeuge. Integrationen müssen unbekannte Felder ignorieren.
- Nicht ohne neue Version: Entfernen oder Umbenennen von Feldern, Ändern von Typen
oder Semantik bestehender Endpunkte, Ändern des Signaturverfahrens. Solche Änderungen
erscheinen als
v2nebenv1;v1bleibt mindestens 12 Monate nach Ankündigung erreichbar. - Strenge Eingabeprüfung: Weil unbekannte Felder mit
422abgelehnt werden, ändert sich das Verhalten bestehender Anfragen nicht, wenn wir neue Felder einführen — sie werden erst gültig, wenn ihr sie bewusst sendet. - Vertragsänderung 29.10.2026:
GET /appointmentsliefertnext_cursorals undurchsichtige Zeichenkette statt als Termin-ID (Details unter „Termine buchen“ in 3.3a). Alte ID-Cursor werden weiter angenommen; wernext_cursorals ID ausgewertet hat, muss es unverändert zurückgeben. - Ankündigungen gehen per E-Mail an die bei uns hinterlegte technische Kontaktadresse des Partners.
11. Anhang
11.1 Status-Werte
| Status | Bedeutung |
|---|---|
| offen | neuer oder unbearbeiteter Lead |
| termin | Termin vereinbart |
| nachfassen | Wiedervorlage nötig |
| ungeeignet | disqualifiziert |
| gewonnen | Abschluss |
| verloren | kein Abschluss |
Der Status ist immer aus dem mapped_status der aktuellen Phase abgeleitet.
11.2 Feld-Limits der REST-API
| Feld | Limit |
|---|---|
title, contact_name, first_name, last_name, email, company |
255 Zeichen |
phone |
50 Zeichen |
source |
100 Zeichen |
notes |
10.000 Zeichen |
tags |
max. 50 Einträge à 100 Zeichen |
value |
0 bis 9.999.999.999 |
probability |
ganze Zahl 0 bis 100 |
setter_id, closer_id |
255 Zeichen, muss aktives Mitglied sein |
text (Notiz) |
10.000 Zeichen |
duplicate_deal_ids |
max. 20 je Aufruf |
search (Query) |
200 Zeichen |
limit (Query) |
1 bis 100 |
11.3 Typen eigener Felder
text, number, select, multiselect, boolean, date, email, phone, url
11.4 Alle Webhook-Ereignisse
deal.created, deal.updated, deal.stage_changed, deal.won, deal.lost,
deal.deleted, appointment.created, appointment.cancelled,
appointment.rescheduled, task.created, task.completed, call.completed,
lead.imported, note.created, funnel.submission_completed, webhook.test
11.5 Glossar Deutsch ↔ API
| Oberfläche | API |
|---|---|
| Workspace | team, team_id |
| Pipeline | pipeline, pipeline_id |
| Phase | stage, stage_id, mapped_status |
| Deal / Lead / Kontakt | deal |
| Eigenes Feld | custom_field, Schlüssel = Feldname |
| Terminart | event_type |
| Termin | appointment |
| Notiz | note (Aktivität vom Typ note) |
| Aufgabe | task |