Integration handbook for partners
Dieses Handbuch richtet sich an Produkt- und Entwicklerteams, die ihr Werkzeug mit Set2Sell Cockpit 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 Cockpit 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; Phasenverlauf, Aktivitäten und Termine 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 67 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.
- Webhook-Signaturen sind HMAC-SHA256 mit einem Secret pro Abo, das ebenfalls nur einmal angezeigt wird.
- Rate-Limits gelten pro Key beziehungsweise pro Absender und werden über Antwort-Header sichtbar gemacht.
- 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 |
✓ | – |
Praktische Folge: Wenn dein Produkt Deals anlegt oder verschiebt und der Kunde erwartet, dass seine Automationen darauf reagieren, ist das heute nur über den Lead-Import-Webhook (beim Anlegen) gegeben. 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).
- 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
Rate-Limit: 120 Anfragen pro Minute und Key. Jede erfolgreiche Antwort (200,
201, 204) und jede 429 trägt diese Header:
| Header | Bedeutung |
|---|---|
X-RateLimit-Limit |
Anfragen pro Minutenfenster (120) |
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 |
Fehlerantworten aus der Route selbst (404, 422, 500) tragen die Zähler-Header
nicht; orientiere dich an der letzten erfolgreichen Antwort. Der Zähler ist über alle
Server-Instanzen geteilt, das Limit gilt also global pro Key.
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):
{ "error": { "code": "validation_error", "message": "pipeline_id: Invalid UUID" } }
| HTTP | code |
Wann |
|---|---|---|
| 401 | unauthorized |
Key fehlt, ist unbekannt oder widerrufen |
| 404 | not_found |
Deal/Pipeline/Abo gehört nicht zum Workspace des Keys oder existiert nicht |
| 422 | validation_error |
Ungültiger Body oder Query-Parameter, unbekanntes Feld, Phase passt nicht zur Pipeline |
| 429 | rate_limited |
Limit erreicht, siehe Retry-After |
| 500 | internal_error |
Datenbankfehler auf unserer Seite |
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.
3.3 Endpunkt-Referenz
| Methode | Pfad | Zweck |
|---|---|---|
| GET | /me |
Verbindungstest, liefert Workspace und Währung des Keys |
| 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.
GET/me
curl https://cockpit.set2sell.io/api/v1/me \
-H "Authorization: Bearer s2s_live_…"
{ "data": { "team": { "id": "3f2c…", "name": "Beispiel GmbH", "currency": "EUR" } } }
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).
Keine Idempotenz: Ein wiederholter POST (etwa durch einen Netzwerk-Retry) legt
einen zweiten Deal an. Es gibt in v1 keinen Idempotency-Key-Header und keine
serverseitige Dublettenprüfung. Integrationen mit eigener Retry-Logik sollten vor dem
Anlegen per GET /deals?search=<email> prüfen oder ihre eigene Deduplizierung führen.
(Der Lead-Import-Webhook in Abschnitt 5 hat eine Dublettenerkennung — das ist der
wesentliche funktionale Unterschied zwischen den beiden 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 außer pipeline_id, 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).
Was PATCH bewusst nicht kann:
pipeline_idändern. Ein Pipeline-Wechsel würde die Phasen-Zuordnung ungültig machen. Dafür einen neuen Deal anlegen.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 |
cursor |
UUID | next_cursor der vorigen Antwort |
{
"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.- Kein Idempotency-Key. Dedupliziere selbst oder nutze den Lead-Import-Webhook.
- Cursor ist nicht chronologisch. Für Deltas
updated_since. - Löschen ist ein Soft-Delete. 30 Tage wiederherstellbar durch den Kunden;
nachvollziehbar über
GET /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 Cockpit 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 Cockpit 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 (67)
| Bereich | Werkzeuge |
|---|---|
| Workspace | list_workspaces, list_workspace_members, invite_workspace_members, list_pending_invitations, revoke_invitation |
| Pipelines | list_pipelines, get_pipeline, 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, list_deal_activities, 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) |
| 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 lösen deal.stage_changed sowie deal.won /
deal.lost aus; 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 | 67 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. - 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 |