Entwickler · REST-API & Webhooks

RevenueLab API v1LIVE

Die RevenueLab REST API gibt dir vollen programmatischen Zugriff auf Leads, Kunden, Projekte und Events. Baue Integrationen mit n8n, Make, Zapier oder eigenen Systemen.

AuthentifizierungBearer-Token im Authorization-Header jedes Requests
HTTP Header
Authorization: Bearer rl_live_DEIN_API_KEY
Key-Format, Verwaltung und Scopes
Base URLJSON rein, JSON raus
Base URL
https://api.revenue-lab.de/v1
Alle Endpunkte auf einen Blick

Übersicht

Schnellstart: API-Key unter Settings → API-Zugang erstellen → Authorization: Bearer rl_live_... → GET https://dashboard.revenue-lab.de/api?__path=projects aufrufen → fertig.
Base URL: https://api.revenue-lab.de/v1
Alle Endpunkte liegen unter /v1/<resource>. Für die API-Übersicht bitte mit abschließendem Schrägstrich abrufen (GET /v1/).

Was kann die API?

  • Leads erstellen, auflisten, filtern, abrufen, aktualisieren
  • Notizen an Leads anheften und Verlauf auslesen
  • Lead → Kunde konvertieren (idempotent)
  • Kunden auflisten und abrufen
  • Projekte auflisten (fuer project_id bei Lead-Erstellung)
  • Event-Log aller API-Calls und Webhook-Zustellungen abfragen
  • Outbound Webhooks — bei Events automatisch Daten an Make / n8n / Zapier pushen
Zwei Richtungen:
REST API (Bearer-Token) = Daten in RevenueLab schreiben.
Webhooks (Automationen) = Events aus RevenueLab heraus an deine Systeme senden.
Erklärungen ohne Code: Wie Lead-Portal, Kunden-Portal, Pipeline und Kundenakte fachlich funktionieren, steht in Hilfe & Anleitungen. Diese Seite beschreibt die Schnittstelle.

Authentifizierung

Jeder Request braucht einen Bearer-Token im Authorization-Header:

HTTP Header
Authorization: Bearer rl_live_abc123def456ghi789jkl012mno345pqrs6789

API-Key Regeln

EigenschaftWert
Formatrl_live_ + 40 Hex-Zeichen = 48 Zeichen total
VerwaltungSettings → API-Zugang
SichtbarkeitNur einmalig nach Erstellung im Klartext sichtbar
LimitMax. 10 aktive Keys pro Account
BerechtigungenPro Key: read, write, delete — siehe Berechtigungen (Scopes)
DeaktivierungJederzeit deaktivierbar oder loeschbar
Sicherheit: API-Keys gehoeren niemals in Client-seitigen Code (Browser-JS, Mobile Apps) oder oeffentliche Repos. In Zapier/Make/n8n immer ueber die dortigen Secret-Felder hinterlegen.

Berechtigungen (Scopes)

Jeder API-Key hat eine Kombination aus drei Berechtigungen. Welche das sind, legst du beim Erstellen des Keys fest und kannst es jederzeit unter Settings → API-Zugang ändern, ohne den Key neu zu erzeugen. Damit bekommt z. B. eine Integration, die nur Daten abfragen soll, einen Nur-Lese-Key.

ScopeErlaubtHTTP-Methoden
readDaten abrufen: Listen, Einzelobjekte, Notizen, Dateien (auch Download), Event-Log, API-RootGET
writeAnlegen und Ändern: Leads, Kunden, Notizen, Dateien hochladen, Lead konvertieren, ToDosPOST, PATCH, PUT
deleteUnwiderruflich löschen: Leads, Kunden, Lead-Dateien, ToDosDELETE
sign:readSign Lab: Dokumente, Vorlagen, Webhooks lesen, signierte PDFs laden (siehe Sign Lab)GET /v1/sign-*
sign:writeSign Lab: anlegen, senden, erinnern, zurückziehen, Webhooks verwaltenPOST, DELETE /v1/sign-*
  • Die Scopes sind unabhängig: write schließt read nicht ein. Ein Key, der nur schreiben darf, kann auch keine Daten abrufen. Beliebige Kombinationen sind möglich, mindestens ein Scope ist Pflicht.
  • OPTIONS (CORS-Preflight) braucht keinen Scope.
  • Neue Keys: Die Scopes werden beim Erstellen ausdrücklich gewählt, vorausgewählt ist nur read.
  • Bestehende Keys (vor Einführung der Scopes erstellt) behalten ihre bisherigen Fähigkeiten: read + write. delete haben sie nicht; es muss ausdrücklich vergeben werden.
  • Abgelehnte Anfragen zählen zum Rate-Limit und erscheinen im Event-Log (Status 403).

Fehlender Scope

Fehlt dem Key der für die Methode nötige Scope, antwortet die API mit HTTP 403 und dem Fehlercode insufficient_scope. Die Meldung nennt den benötigten Scope und die Scopes des Keys:

403 Forbidden
{
  "success": false,
  "error": {
    "code":    "insufficient_scope",
    "message": "Dieser API-Key hat keine Berechtigung für DELETE-Anfragen. Benötigter Scope: 'delete'. Scopes dieses Keys: read, write. ..."
  }
}
Empfehlung: Pro Integration einen eigenen Key mit den kleinstmöglichen Rechten anlegen, z. B. read für Reporting, read + write für einen CRM-Sync, delete nur, wenn die Integration wirklich löschen soll.

Base URL & Format

Base URL
https://api.revenue-lab.de/v1

Endpunkte werden als REST-Pfade angesprochen:

  • GET /v1/leads — Leads auflisten
  • POST /v1/leads — Lead erstellen
  • GET /v1/leads/42 — Einzelner Lead
  • PATCH /v1/leads/42 — Lead aktualisieren
  • POST /v1/leads/42/notes — Notiz hinzufuegen
  • POST /v1/leads/42/convert — Lead zum Kunden machen

Alle Anfragen und Antworten verwenden JSON. Bei POST/PATCH den Header Content-Type: application/json setzen.

Erfolgreiche Antwort (Einzelobjekt)

JSON
{
  "success": true,
  "data": {
    "id": 42,
    "name": "Max Mustermann"
  }
}

Erfolgreiche Antwort (Liste mit Pagination)

JSON
{
  "success": true,
  "data":  [/* Array von Objekten */],
  "total": 84,
  "limit": 50,
  "offset": 0
}

HTTP Status Codes

200OK — Anfrage erfolgreich verarbeitet
201Created — Ressource erfolgreich erstellt (POST)
400Bad Request — Fehlende oder ungueltige Parameter
401Unauthorized — API-Key fehlt, ungueltig oder deaktiviert
403Forbidden — dem API-Key fehlt der nötige Scope (insufficient_scope)
404Not Found — Ressource existiert nicht
405Method Not Allowed — HTTP-Methode fuer diese Route nicht erlaubt

Fehlerbehandlung

Fehlerantworten haben immer "success": false und ein error-Objekt mit maschinenlesbarem Code:

Fehlerantwort
{
  "success": false,
  "error": {
    "code":    "missing_field",
    "message": "project_id ist erforderlich."
  }
}

Fehlercodes

missing_api_key
Kein oder falsches Key-Format im Header
invalid_api_key
Key existiert nicht in der Datenbank
revoked_api_key
Key wurde deaktiviert
insufficient_scope
HTTP 403 — dem Key fehlt der Scope (read/write/delete) für diese Methode, siehe Scopes
missing_field
Ein Pflichtfeld fehlt im Request Body
missing_fields
Mehrere Pflichtfelder fehlen
invalid_status
Status-Wert nicht in der erlaubten Liste
invalid_json
Request Body ist kein gueltiges JSON
project_not_found
project_id existiert nicht fuer diesen Account
not_found
Angeforderte Ressource nicht gefunden
no_fields
PATCH ohne Felder zum Aktualisieren
method_not_allowed
HTTP-Methode fuer diese Route nicht erlaubt
unknown_resource
Unbekannter API-Pfad
unknown_subresource
Unbekannte Sub-Resource, z.B. /leads/42/foo
invalid_id
ID im Pfad ist nicht numerisch oder fehlt (z.B. /leads//notes)
invalid_datetime
Ein Datumsfeld ist nicht parsebar
lead_has_contracts
HTTP 409 — Lead hat Verträge, DELETE abgelehnt
customer_has_billing
HTTP 409 — Kunde hat Rechnungen/Abos, DELETE abgelehnt
delete_failed
Löschen ist fehlgeschlagen (nichts wurde halb gelöscht)
updated_since_unavailable
updated_since ist auf diesem System noch nicht verfügbar
rate_limited
HTTP 429 — Rate-Limit erreicht (siehe unten)

Rate-Limit

Pro API-Key sind 600 Anfragen pro Stunde erlaubt. Danach antwortet die API mit HTTP 429 und dem Fehlercode rate_limited, bis das Zeitfenster abgelaufen ist. Die Antworten enthalten aktuell keine X-RateLimit-*-Header.

Endpoint-Uebersicht

Alle verfuegbaren Endpunkte auf einen Blick. Base URL: https://api.revenue-lab.de/v1

GET /v1/projects — Alle Projekte auflisten

GET /v1/leads — Leads auflisten (Filter: project_id, status, search, external_ref, updated_since, include)
GET /v1/leads/{id} — Einzelnen Lead abrufen
POST /v1/leads — Neuen Lead erstellen
PATCH /v1/leads/{id} — Lead-Felder aktualisieren
DEL /v1/leads/{id} — Lead löschen (409 bei Verträgen)
GET /v1/leads/{id}/notes — Notizen-Verlauf abrufen
POST /v1/leads/{id}/notes — Notiz hinzufuegen
POST /v1/leads/{id}/convert — Lead zum Kunden konvertieren
GET /v1/leads/{id}/files — Dateien zu einem Lead auflisten
POST /v1/leads/{id}/files — Datei hochladen (multipart/Base64; optional as_contract=1 für Verträge-Tab)
GET /v1/leads/{id}/files/{fid}/download — Datei downloaden
DEL /v1/leads/{id}/files/{fid} — Datei löschen

GET /v1/customers — Kunden auflisten (Filter: search, updated_since, include)
GET /v1/customers/{id} — Einzelnen Kunden abrufen
PATCH /v1/customers/{id} — Kunde aktualisieren (Standardfelder + custom)
DEL /v1/customers/{id} — Kunde löschen (409 bei Rechnungen/Abos)
GET /v1/customers/{id}/notes — Kunden-Feed / Notizen abrufen
POST /v1/customers/{id}/notes — Notiz hinzufuegen (auch bei Kunden ohne lead_id)

GET /v1/todos — ToDos auflisten (Filter: column_id, assigned_user_id, completed)
GET /v1/todos/{id} — Einzelne ToDo abrufen
POST /v1/todos — ToDo anlegen (feuert todo.created)
PATCH /v1/todos/{id} — ToDo aktualisieren (feuert todo.updated / todo.completed)
DEL /v1/todos/{id} — ToDo loeschen

GET /v1/events — Event-Log (Filter: category=api|webhook)
Fuer KI-Agents & Automationen: Alle Endpunkte akzeptieren application/json und geben JSON zurueck. Authentifizierung immer via Authorization: Bearer rl_live_.... Pagination ueber limit (max 200) + offset. Fehler liefern error.code (maschinenlesbar) + error.message (deutsch, menschenlesbar).

Projekte auflisten

Gibt alle Projekte zurueck. Die id wird als project_id beim Erstellen von Leads benoetigt.

GET https://api.revenue-lab.de/v1/projects

Antwort-Felder

FeldTypBeschreibung
idintegerEindeutige Projekt-ID
namestringProjektname
typestringInterner Projekttyp
delivery_typestringLieferart: termine, leads (Lead-Lieferung an einen Auftraggeber) oder eigen (Inhouse-Akquise). Siehe Lead-Lieferung.
lead_valuenumber|nullPreis je Termin in Euro
calendar_linkstring|nullBuchungslink des Auftrags
agent_idinteger|nullZugeordneter Agent
200 OK
{
  "success": true,
  "data": [
    {
      "id":          1,
      "name":        "Outbound B2B DACH",
      "description": "Kaltakquise Kampagne Q3 2026",
      "status":      "active",
      "lead_value":  250.00,
      "created_at":  "2026-04-01 09:00:00"
    },
    {
      "id":          2,
      "name":        "Inbound Website-Leads",
      "description": "Kontaktformular und Landingpages",
      "status":      "active",
      "lead_value":  180.00,
      "created_at":  "2026-05-15 10:30:00"
    }
  ],
  "total": 2
}
cURL
curl https://api.revenue-lab.de/v1/projects \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"
Python
import requests

API_KEY = "rl_live_DEIN_API_KEY"
BASE    = "https://api.revenue-lab.de/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}

res = requests.get(f"{BASE}/projects", headers=headers)
projects = res.json()["data"]

for p in projects:
    print(f"{p['id']}: {p['name']}")

Leads auflisten

Paginierte Liste von Leads. Unterstuetzt Filter nach Projekt, Status und Freitext-Suche.

GET https://api.revenue-lab.de/v1/leads

Query-Parameter

ParameterTypBeschreibung
project_idintegerOptionalNur Leads aus diesem Projekt
statusstringOptionalExakter Status-Match, z.B. Neu oder Terminiert
searchstringOptionalFreitextsuche in Name, Firma, Telefon, E-Mail
external_refstringOptionalExakter Treffer auf deinen Fremd-Schlüssel — ein Lead per Referenz nachschlagen
updated_sincedatetimeOptionalNur Leads, die seit diesem Zeitpunkt angelegt oder geändert wurden (inklusive). Sortierung dann aufsteigend nach updated_at. Siehe Delta-Sync unten.
includestringOptionalcustom liefert je Lead die Custom-Field-Werte mit
limitintegerOptionalErgebnisse pro Seite (1–200, Standard: 50)
offsetintegerOptionalStartposition (Standard: 0)

Delta-Sync (CRM-Abgleich)

Mit updated_since holst du nur, was sich seit deinem letzten Lauf geändert hat. Die Liste ist dann stabil nach updated_at (aufsteigend), bei Gleichstand nach id sortiert.

  • Merke dir updated_at des letzten Eintrags als Startpunkt des nächsten Laufs. Die Grenze ist inklusive: entdoppele Treffer über die id.
  • Zeitangaben sind Serverzeit (Europe/Berlin). ISO-8601 mit Zeitzone (2026-09-21T10:00:00Z) wird korrekt umgerechnet.
  • Gelöschte Leads erscheinen dort nicht. Gleiche die IDs gelegentlich vollständig ab, wenn du Löschungen erkennen willst.
GET /v1/leads?updated_since=2026-09-21T10:00:00&limit=200
200 OK
{
  "success": true,
  "data": [
    {
      "id":             42,
      "project_id":     1,
      "project_name":   "Outbound B2B DACH",
      "name":           "Max Mustermann",
      "company":        "Mustermann GmbH",
      "phone":          "+49 221 1234567",
      "email":          "max@mustermann-gmbh.de",
      "position":       "Geschaeftsfuehrer",
      "address":        "Musterstrasse 1, 50667 Koeln",
      "website":        "www.mustermann-gmbh.de",
      "status":         "Neu",
      "notes":          "Kontakt ueber Kontaktformular",
      "followup_at":    null,
      "pipeline_stage": "In Auftrag",
      "deal_value":     250.00,
      "created_at":     "2026-06-01 09:15:00",
      "updated_at":     "2026-06-01 09:15:00"
    },
    {
      "id":             43,
      "project_id":     1,
      "project_name":   "Outbound B2B DACH",
      "name":           "Lisa Schmidt",
      "company":        "Schmidt & Partner AG",
      "phone":          "+49 30 9876543",
      "email":          "l.schmidt@schmidt-partner.de",
      "position":       "Vertriebsleiterin",
      "address":        null,
      "website":        "www.schmidt-partner.de",
      "status":         "Terminiert",
      "notes":          "Termin am 10.06. um 14:00 Uhr",
      "followup_at":    "2026-06-10 14:00:00",
      "pipeline_stage": "Terminiert",
      "deal_value":     500.00,
      "created_at":     "2026-06-02 11:30:00",
      "updated_at":     "2026-06-04 16:20:00"
    }
  ],
  "total":  84,
  "limit":  50,
  "offset": 0
}
cURL
# Alle Leads
curl "https://api.revenue-lab.de/v1/leads" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"

# Filtern: Projekt 1, Status "Neu", Seite 2
curl "https://api.revenue-lab.de/v1/leads?project_id=1&status=Neu&limit=50&offset=50" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"

# Freitextsuche
curl "https://api.revenue-lab.de/v1/leads?search=Mustermann" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"
Python
import requests

API_KEY = "rl_live_DEIN_API_KEY"
BASE    = "https://api.revenue-lab.de/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}

# Alle neuen Leads aus Projekt 1
res = requests.get(f"{BASE}/leads", headers=headers, params={
    "project_id": 1,
    "status": "Neu",
    "limit": 50
})
data = res.json()
print(f"{len(data['data'])} von {data['total']} Leads geladen")
JavaScript
const API_KEY = 'rl_live_DEIN_API_KEY';
const BASE    = 'https://api.revenue-lab.de/v1';

const params = new URLSearchParams({
  project_id: 1, status: 'Neu', limit: 50
});
const res = await fetch(`${BASE}/leads?${params}`, {
  headers: { 'Authorization': `Bearer ${API_KEY}` },
});
const { success, data, total } = await res.json();
console.log(`${data.length} von ${total} Leads geladen`);

Einzelnen Lead abrufen

GET https://api.revenue-lab.de/v1/leads/{id}
200 OK
{
  "success": true,
  "data": {
    "id":             42,
    "project_id":     1,
    "project_name":   "Outbound B2B DACH",
    "name":           "Max Mustermann",
    "company":        "Mustermann GmbH",
    "phone":          "+49 221 1234567",
    "email":          "max@mustermann-gmbh.de",
    "position":       "Geschaeftsfuehrer",
    "address":        "Musterstrasse 1, 50667 Koeln",
    "website":        "www.mustermann-gmbh.de",
    "status":         "Neu",
    "notes":          "Kontakt ueber Kontaktformular",
    "followup_at":    null,
    "pipeline_stage": "In Auftrag",
    "deal_value":     250.00,
    "created_at":     "2026-06-01 09:15:00",
    "updated_at":     "2026-06-01 09:15:00"
  }
}
404 Not Found
{
  "success": false,
  "error": {
    "code":    "not_found",
    "message": "Lead mit ID 9999 nicht gefunden."
  }
}
cURL
curl "https://api.revenue-lab.de/v1/leads/42" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"

Lead erstellen

Erstellt einen neuen Lead. Pflicht: project_id + mindestens ein Kontaktfeld (name, company, phone oder email).

POST https://api.revenue-lab.de/v1/leads Gibt 201 + das erstellte Lead-Objekt zurueck

Request Body

FeldTypBeschreibung
project_idintegerPflichtProjekt-ID (via GET /v1/projects abrufbar)
namestringOptional*Vor- und Nachname
companystringOptional*Firmenname
phonestringOptional*Telefonnummer (freies Format, z.B. +49 221 1234567)
emailstringOptional*E-Mail-Adresse
positionstringOptionalPosition / Jobtitel
addressstringOptionalAdresse
websitestringOptionalWebsite-URL
statusstringOptionalLead-Status (Standard: Neu). Siehe Status-Werte.
notesstringOptionalNotizen zum Lead
followup_atdatetimeOptionalWiedervorlage-Zeitpunkt (2026-06-10T14:00:00)
deal_valuenumberOptionalLead-Wert in Euro (Standard: Projekt-Standardwert)
external_refstringOptionalEigener Fremd-Schlüssel (z.B. die ID im Quellsystem), pro Account eindeutig. Aktiviert den Upsert.
sourcestringOptionalHerkunft des Leads (frei wählbar, z.B. pipedrive)
source_notestringOptionalZusatzinfo zur Herkunft
pipeline_stagestringOptionalPipeline-Stage. Ohne Angabe wird sie aus dem status abgeleitet (siehe Status-Werte).
lead_typestringOptionalb2b (Standard) oder b2c
salutationstringOptionalAnrede
first_namestringOptionalVorname
last_namestringOptionalNachname
legal_formstringOptionalRechtsform (z.B. GmbH)
termin_atdatetimeOptionalVereinbarter Termin
termin_notesstringOptionalNotizen zum Termin

* Mindestens eines von name, company, phone oder email muss angegeben werden. project_id ist Pflicht, außer es wird ein external_ref gesendet.

Upsert über external_ref (Idempotenz)

Sendest du einen external_ref, der in deinem Account schon existiert, wird kein neuer Lead angelegt, sondern der vorhandene aktualisiert. Die Antwort ist dann 200 statt 201 und enthält "upserted": true. So kannst du denselben Datensatz gefahrlos mehrfach senden (z.B. bei einem Sync oder Retry).

  • Nur gesendete Felder werden geändert. Was im Body fehlt, bleibt unverändert — genau wie bei PATCH.
  • Ein explizit gesendeter leerer Wert (null oder "") leert das Feld. Ausgenommen sind name, termin_at und termin_notes: sie werden durch leere Werte nie gelöscht.
  • Wird status gesendet, aber keine pipeline_stage, wird sie wie üblich aus dem Status abgeleitet.
  • Das project_id eines vorhandenen Leads ändert ein Upsert nicht — dafür PATCH /v1/leads/{id} verwenden.
  • Beim Anlegen gelten weiter die Pflichtangaben (mindestens ein Kontaktfeld).
cURL
curl -X POST "https://api.revenue-lab.de/v1/leads" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": 1,
    "name":       "Max Mustermann",
    "company":    "Mustermann GmbH",
    "phone":      "+49 221 1234567",
    "email":      "max@mustermann-gmbh.de",
    "position":   "Geschaeftsfuehrer",
    "notes":      "Anfrage ueber Kontaktformular, Interesse an Paket Business",
    "status":     "Neu"
  }'
Python
import requests

API_KEY = "rl_live_DEIN_API_KEY"
BASE    = "https://api.revenue-lab.de/v1"
headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type":  "application/json"
}

lead = {
    "project_id": 1,
    "name":       "Max Mustermann",
    "company":    "Mustermann GmbH",
    "phone":      "+49 221 1234567",
    "email":      "max@mustermann-gmbh.de",
    "position":   "Geschaeftsfuehrer",
    "notes":      "Anfrage ueber Kontaktformular"
}

res = requests.post(f"{BASE}/leads", json=lead, headers=headers)
result = res.json()

if result["success"]:
    print(f"Lead erstellt: ID {result['data']['id']}")
else:
    print(f"Fehler: {result['error']['message']}")
JavaScript
const res = await fetch('https://api.revenue-lab.de/v1/leads', {
  method:  'POST',
  headers: {
    'Authorization': 'Bearer rl_live_DEIN_API_KEY',
    'Content-Type':  'application/json',
  },
  body: JSON.stringify({
    project_id: 1,
    name:       'Max Mustermann',
    company:    'Mustermann GmbH',
    phone:      '+49 221 1234567',
    email:      'max@mustermann-gmbh.de',
    notes:      'Anfrage ueber Kontaktformular',
  }),
});

const { success, data, error } = await res.json();
if (success) console.log('Lead erstellt:', data.id);
else console.error(error.message);
201 Created
{
  "success": true,
  "data": {
    "id":             42,
    "project_id":     1,
    "name":           "Max Mustermann",
    "company":        "Mustermann GmbH",
    "phone":          "+49 221 1234567",
    "email":          "max@mustermann-gmbh.de",
    "position":       "Geschaeftsfuehrer",
    "status":         "Neu",
    "pipeline_stage": "In Auftrag",
    "deal_value":     250.00,
    "created_at":     "2026-06-05 10:30:00",
    "updated_at":     "2026-06-05 10:30:00"
  }
}

Lead aktualisieren

Aktualisiert einzelne Felder eines Leads. Nur gesendete Felder werden geaendert — alles andere bleibt unberuehrt.

PATCH https://api.revenue-lab.de/v1/leads/{id} Gibt das aktualisierte Lead-Objekt zurueck

Aktualisierbare Felder

Alle Felder sind optional — sende nur was sich ändert. Unbekannte Keys werden silent ignoriert (kein Fehler).

Stammdaten

FeldTypBeschreibung
namestringVoller Name (Anzeige)
salutationstringAnrede (Herr / Frau / Divers)
first_namestringVorname
last_namestringNachname
companystringFirma
legal_formstringRechtsform (GmbH, UG, AG, ...)
positionstringPosition / Jobtitel
lead_typestringb2b oder b2c
phonestringTelefon
emailstringE-Mail
websitestringWebsite

Adresse

FeldTypBeschreibung
addressstringKomplette Adresse als String (Legacy — wenn gesetzt, werden Einzelfelder ignoriert)
streetstringStraße + Hausnummer
zipstringPostleitzahl
citystringOrt
countrystringLand

Sales-Pipeline & Deal

FeldTypBeschreibung
statusstringLead-Status. pipeline_stage wird automatisch synchronisiert (siehe Auto-Logik).
pipeline_stagestringPipeline-Stage explizit setzen. Unterstützt Custom-Stages. Bei is_won-Stages: Auto-Convert greift.
lost_reasonstring|nullVerlust-Grund (Freitext, nur sinnvoll bei status=Verloren)
deal_valuenumber|nullDeal-Wert in Euro
deal_probabilityint 0-100|nullAbschlusswahrscheinlichkeit in Prozent. Wird bei "Gewonnen" automatisch auf 100 gesetzt.
expected_close_atdate|nullVoraussichtliches Abschluss-Datum (YYYY-MM-DD). Bei "Gewonnen" und vorher leer: heute.
sourcestringLead-Quelle (Cold-Call, LinkedIn, Empfehlung, ...)
project_idint|nullAuftrag/Projekt-Zuordnung. Wird gegen Tenant validiert.

Termine & Datumsfelder

Alle Datetime-Felder akzeptieren: ISO-8601 (2026-07-01T09:00:00), MySQL-Format (2026-07-01 09:00:00), nur Datum (2026-07-01), null zum Entfernen, oder den Shortcut-String "NOW" (Server-Zeit).

FeldTypBeschreibung
followup_atdatetime|nullWiedervorlage-Datum (für Auto-Dialer-Queue)
termin_atdatetime|nullVereinbarter Termin mit dem Lead
appointment_set_atdatetime|nullZeitpunkt der Terminvereinbarung (für Reports)

Sonstiges

FeldTypBeschreibung
notesstring|nullNotiz-Feld (ersetzt den bestehenden Text. Für append: POST /v1/leads/{id}/notes).
customobjectCustom-Field-Werte als {"field_key": "value"}. Siehe Custom Fields.
Status ↔ Pipeline-Stage Synchronisation:
Wenn du status setzt, wird pipeline_stage automatisch mitgesetzt: Bad Data→Bad Data, Gewonnen→Gewonnen, Verloren→Verloren, alles andere→In Auftrag. Setzt du pipeline_stage auf eine is_won-Custom-Stage, wird auch status="Gewonnen" implizit gesetzt. Explizite Werte schlagen Auto-Logik immer.
Auto-Convert bei "Gewonnen": Wechselt der Lead in eine is_won-Stage, wird automatisch ein Kunde angelegt (idempotent), deal_probability=100 gesetzt und expected_close_at=heute (sofern leer). Details: Auto-Logik bei "Gewonnen".
cURL — Termin setzen
# Status auf "Terminiert" setzen + Wiedervorlage eintragen
curl -X PATCH "https://api.revenue-lab.de/v1/leads/42" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status":      "Terminiert",
    "notes":       "Termin am 10.06. um 14:00 bestaetigt",
    "followup_at": "2026-06-10T14:00:00",
    "termin_at":   "2026-06-10T14:00:00"
  }'

# Lead als "Gewonnen" markieren — löst Auto-Convert zu Kunde aus
# deal_probability=100 + expected_close_at=heute werden automatisch gesetzt
curl -X PATCH "https://api.revenue-lab.de/v1/leads/42" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status":     "Gewonnen",
    "deal_value": 12500,
    "custom": {
      "angebotspaket":  "LinkedIn Starter",
      "kampagnenstart": "2026-07-01",
      "laufzeit":       "9 Monate"
    }
  }'
Python
import requests

API_KEY = "rl_live_DEIN_API_KEY"
BASE    = "https://api.revenue-lab.de/v1"
headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type":  "application/json"
}

lead_id = 42
update  = {
    "status":      "Terminiert",
    "notes":       "Termin am 10.06. um 14:00 bestaetigt",
    "followup_at": "2026-06-10T14:00:00"
}

res = requests.patch(f"{BASE}/leads/{lead_id}", json=update, headers=headers)
result = res.json()
print(f"Neuer Status: {result['data']['status']}")
200 OK — Won-Szenario
{
  "success": true,
  "data": {
    "id":                42,
    "name":              "Max Mustermann",
    "company":           "Mustermann GmbH",
    "status":            "Gewonnen",
    "pipeline_stage":    "Gewonnen",
    "deal_value":        "12500.00",
    "deal_probability":  100,           // auto-gesetzt
    "expected_close_at": "2026-06-06",  // auto-gesetzt (heute)
    "custom": {
      "angebotspaket":  "LinkedIn Starter",
      "kampagnenstart": "2026-07-01",
      "laufzeit":       "9 Monate"
    }
    // ...alle weiteren Felder
  },
  "kunde_id":     142,                  // neu angelegter Kunde
  "custom_saved": ["angebotspaket", "kampagnenstart", "laufzeit"]
}

Lead löschen

Löscht einen Lead endgültig, mit demselben Verhalten wie „Lead löschen“ im CRM. Es gibt keinen Papierkorb.

DELETE https://api.revenue-lab.de/v1/leads/{id}
  • Mitgelöscht: Anruf-Historie (Aufnahmen werden auch bei Twilio gelöscht), Dateien, Notiz-Verlauf, Kontakte, Angebote, Aufgaben, Zuständigkeiten und Custom-Field-Werte.
  • Bleibt bestehen: ein verknüpfter Kunde (lead_id wird leer) und der E-Mail-Verlauf im Postfach.
  • Hat der Lead Verträge, antwortet die API mit 409 lead_has_contracts und löscht nichts — bitte im CRM prüfen.
  • Der Event-Log speichert nur die ID und den external_ref, keine weiteren Daten des Leads.
cURL
curl -X DELETE "https://api.revenue-lab.de/v1/leads/42" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"
200 Response
{
  "success": true,
  "data": { "deleted": true, "id": 42, "external_ref": "pipedrive:1234" }
}

Datei löschen

DELETE https://api.revenue-lab.de/v1/leads/{id}/files/{fid}

Entfernt die Datei vom Server und aus dem Lead. Im Verlauf des Leads bleibt ein Eintrag „Datei gelöscht“. Antwort: {"success":true,"data":{"deleted":true,"id":17,"lead_id":42}}

cURL
curl -X DELETE "https://api.revenue-lab.de/v1/leads/42/files/17" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"

Notizen

Notizen landen im Verlauf eines Leads (identisch mit dem Notizen-Feed im CRM-Drawer). Ideal fuer automatische Status-Updates aus Workflows.

Notiz hinzufuegen

POST https://api.revenue-lab.de/v1/leads/{id}/notes Gibt 201 zurueck
FeldTypBeschreibung
note string Pflicht Text der Notiz (max. 5000 Zeichen). Alias: text.
cURL
curl -X POST "https://api.revenue-lab.de/v1/leads/42/notes" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "note": "Angebot per Mail versendet. Rueckruf in 3 Tagen." }'
Python
import requests

API_KEY = "rl_live_DEIN_API_KEY"
BASE    = "https://api.revenue-lab.de/v1"
headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type":  "application/json"
}

lead_id = 42
note    = {"note": "Angebot per Mail versendet. Rueckruf in 3 Tagen."}

res = requests.post(f"{BASE}/leads/{lead_id}/notes", json=note, headers=headers)
print(res.json())  # {"success": true, "data": {...}}
201 Created
{
  "success": true,
  "data": {
    "id": 187,
    "lead_id": 42,
    "note": "Angebot per Mail versendet. Rueckruf in 3 Tagen.",
    "created_at": "2026-06-05 14:30:00"
  }
}

Notizen-Verlauf abrufen

GET https://api.revenue-lab.de/v1/leads/{id}/notes Neueste zuerst, max. 100
200 OK
{
  "success": true,
  "data": [
    {
      "note": "Angebot per Mail versendet. Rueckruf in 3 Tagen.",
      "created_at": "2026-06-05 14:30:00",
      "user_name": null    // null = via API erstellt
    },
    {
      "note": "Erstanruf: Interesse an Paket Business, Angebot gewuenscht",
      "created_at": "2026-06-03 10:15:00",
      "user_name": "Tom Weber"   // von einem CRM-User erstellt
    }
  ],
  "total": 2
}

Lead → Kunde konvertieren

Erstellt aus einem Lead einen Kunden-Datensatz. Idempotent: existiert bereits ein Kunde fuer diesen Lead, wird dieser zurueckgegeben ("created": false).

POST https://api.revenue-lab.de/v1/leads/{id}/convert Gibt 201 + Kunden-Objekt zurueck
FeldTypBeschreibung
mark_won boolean Optional Setzt den Lead zusaetzlich auf Status Gewonnen. Default: true.
cURL
curl -X POST "https://api.revenue-lab.de/v1/leads/42/convert" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mark_won": true }'
Python
import requests

API_KEY = "rl_live_DEIN_API_KEY"
BASE    = "https://api.revenue-lab.de/v1"
headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type":  "application/json"
}

lead_id = 42
res = requests.post(
    f"{BASE}/leads/{lead_id}/convert",
    json={"mark_won": True},
    headers=headers
)
result = res.json()
if result["data"]["created"]:
    print(f"Neuer Kunde: ID {result['data']['kunde']['id']}")
else:
    print("Kunde existierte bereits")
201 Created
{
  "success": true,
  "data": {
    "created": true,
    "kunde": {
      "id":      12,
      "name":    "Max Mustermann",
      "firma":   "Mustermann GmbH",
      "lead_id": 42
    }
  }
}

Dateien hochladen

Haenge Dateien (PDF-Angebote, Vertraege, Screenshots, Sprachnotizen-Transkripte, ...) direkt an einen Lead. Die Datei landet im gleichen Dokumenten-Bereich wie ein manueller Upload im CRM-Drawer.

Zwei Upload-Methoden:
1. multipart/form-data — klassisch wie ein HTML-Formular (z.B. curl -F file=@...)
2. application/json mit Base64 — ideal fuer n8n/Make/Zapier ohne Multipart-Support

Limits & erlaubte Typen

  • Max. Groesse: 20 MB pro Datei
  • Bilder: JPG, PNG, GIF, WebP
  • Dokumente: PDF, DOC(X), XLS(X), PPT(X), TXT, CSV
  • Archive: ZIP
  • Nicht erlaubt: SVG (XSS-Risk), EXE, JS

Optionale Felder: Upload als Vertrag

Mit dem optionalen Flag as_contract=1 landet die Datei zusätzlich im Verträge-Tab des Leads (nicht nur unter "Dateien"). Ideal für PandaDoc/DocuSign-Workflows, bei denen die unterzeichnete PDF zurückkommt.

FeldTypDefaultBeschreibung
as_contractboolfalseWenn 1/true/yes: zusätzlicher Eintrag in contracts-Tabelle.
contract_titlestringFilename ohne EndungTitel im Verträge-Tab (z. B. "LinkedIn Premium 9M")
contract_recipient_namestringLead-NameEmpfänger-Anzeige im Verträge-Tab
contract_recipient_emailstringLead-E-MailEmpfänger-E-Mail im Verträge-Tab
contract_statusstringcompletedpending | completed | declined | expired. Bei completed: signed_at wird auf jetzt gesetzt.
Mit as_contract=1 + Setting "Auto-Won": Wenn Settings → Documenso → Lead auto auf Gewonnen aktiv ist, löst der Upload die komplette Won-Kaskade aus: Pipeline-Stage → Gewonnen, Kunde wird angelegt, deal_probability=100, expected_close_at=heute. Ein einziger HTTP-Call schließt den ganzen Deal ab.

Datei hochladen (Multipart)

POST https://api.revenue-lab.de/v1/leads/{id}/files Content-Type: multipart/form-data — Feld file
cURL — Multipart
curl -X POST "https://dashboard.revenue-lab.de/api?__path=leads/42/files" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -F "file=@/pfad/zu/angebot.pdf"
Python — Multipart
import requests

API_KEY = "rl_live_DEIN_API_KEY"
BASE    = "https://dashboard.revenue-lab.de/api"
headers = {"Authorization": f"Bearer {API_KEY}"}

lead_id = 42
with open("angebot.pdf", "rb") as f:
    res = requests.post(
        f"{BASE}?__path=leads/{lead_id}/files",
        headers=headers,
        files={"file": ("angebot.pdf", f, "application/pdf")}
    )

print(res.json())
# → {"success": true, "data": {"id": 17, "lead_id": 42, "orig_name": "angebot.pdf",
#                              "filesize": 234567, "download_url": "..."}}
JavaScript — FormData
const fd = new FormData();
fd.append('file', fileInput.files[0]);

const res = await fetch(
  'https://dashboard.revenue-lab.de/api?__path=leads/42/files',
  {
    method: 'POST',
    headers: { 'Authorization': 'Bearer rl_live_DEIN_API_KEY' },
    body: fd
  }
);
const { data } = await res.json();
console.log('Datei-ID:', data.id);
201 Created
{
  "success": true,
  "data": {
    "id":           17,
    "lead_id":      42,
    "orig_name":    "angebot.pdf",
    "mime_type":    "application/pdf",
    "filesize":     234567,
    "created_at":   "2026-06-05 21:00:00",
    "download_url": "https://api.revenue-lab.de/v1/leads/42/files/17/download"
  }
}

Datei hochladen (JSON + Base64 — fuer n8n/Make/Zapier)

Wenn deine Plattform kein Multipart kann, sende JSON mit filename + content_base64:

cURL — Base64
# Datei vorher in Base64 konvertieren
B64=$(base64 -w0 angebot.pdf)

curl -X POST "https://dashboard.revenue-lab.de/api?__path=leads/42/files" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"filename\": \"angebot.pdf\", \"content_base64\": \"$B64\"}"
Python — Base64
import requests, base64

API_KEY = "rl_live_DEIN_API_KEY"
BASE    = "https://dashboard.revenue-lab.de/api"
headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type":  "application/json"
}

with open("angebot.pdf", "rb") as f:
    b64 = base64.b64encode(f.read()).decode("ascii")

payload = {"filename": "angebot.pdf", "content_base64": b64}
res = requests.post(f"{BASE}?__path=leads/42/files", json=payload, headers=headers)
print(res.json())
n8n — HTTP Request Node
# n8n HTTP Request Node Konfiguration

Method:          POST
URL:             https://dashboard.revenue-lab.de/api?__path=leads/{{$json.lead_id}}/files
Authentication:  Header Auth
  Header Name:   Authorization
  Header Value:  Bearer rl_live_DEIN_API_KEY
Send Body:       Yes
Body Type:       JSON
JSON Body:
{
  "filename": "{{$node['PandaDoc'].json['document_name']}}",
  "content_base64": "{{$node['PandaDoc'].json['pdf_base64']}}"
}

# Antwort: data.download_url kann direkt weiterverarbeitet werden

Dateien auflisten

GET https://api.revenue-lab.de/v1/leads/{id}/files Liste aller Dateien zu einem Lead (max. 200)
cURL
curl "https://dashboard.revenue-lab.de/api?__path=leads/42/files" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"
200 OK
{
  "success": true,
  "data": [
    {
      "id":           17,
      "orig_name":    "angebot.pdf",
      "mime_type":    "application/pdf",
      "filesize":     234567,
      "uploaded_by":  0,
      "created_at":   "2026-06-05 21:00:00",
      "download_url": "https://api.revenue-lab.de/v1/leads/42/files/17/download"
    }
  ],
  "total": 1
}

Datei downloaden

GET https://api.revenue-lab.de/v1/leads/{id}/files/{file_id}/download Liefert die Datei direkt als Binary mit Original-MIME-Type
cURL
curl "https://dashboard.revenue-lab.de/api?__path=leads/42/files/17/download" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -o angebot.pdf
Auth bleibt Pflicht: Auch der Download verlangt einen gueltigen Bearer-Token. Die download_url ist nicht oeffentlich teilbar — ohne Header gibt's 401.

Lead-Lieferung an Auftraggeber

Erzeugt ihr Leads für einen Auftraggeber, legt ihr dafür einen Auftrag mit Lieferart Leads an. Jeder Lead, der in diesem Auftrag landet, gilt sofort als geliefert: Er erscheint im Kunden-Portal des Auftraggebers und intern im Lead-Portal. Fachliche Erklärung: Wiki: Das Lead-Portal.

So lieferst du per API

  1. Mit GET /v1/projects den Auftrag suchen, dessen delivery_type den Wert leads hat.
  2. Lead mit POST /v1/leads und dieser project_id anlegen. Mit external_ref bleibt das idempotent, ein zweiter Aufruf liefert denselben Lead (siehe Upsert).
  3. Fertig: Der Auftraggeber sieht den Lead im Portal, euer Team im Arbeitsplatz und im Lead-Portal.
POST /v1/leads
{
  "project_id": 12,
  "external_ref": "formular:2026-10-18:4711",
  "company": "Beispiel GmbH",
  "first_name": "Max",
  "last_name": "Muster",
  "phone": "+49 30 1234567",
  "email": "max@example.com"
}

Was du wissen musst

  • Die API sieht alle Leads. In der Oberfläche stehen Leads aus Aufträgen für Auftraggeber (Lieferart leads, sowie termine mit verknüpftem Auftraggeber) nicht im CRM (Leads-Liste, Pipeline, CRM-Suche), sondern im Lead-Portal, außer die Organisation bündelt unter Einstellungen → Lead-Ordnung alle Leads im CRM. GET /v1/leads liefert sie trotzdem; filtere bei Bedarf mit project_id oder prüfe die Lieferart des Projekts.
  • Gezählt wird ab dem im Portal eingestellten Lieferbeginn und ohne Leads mit Status Bad Data. Setzt ihr einen Lead per API auf Bad Data, zählt er nicht zur Lieferung.
  • Rückmeldung und Reklamationen des Auftraggebers werden im Kunden-Portal gesetzt und im Lead-Portal entschieden. In der REST-API sind sie nicht enthalten; für KI-Agenten gibt es dazu die MCP-Werkzeuge für das Lead-Portal (lesen, Entscheidung vorschlagen, Abrechnungszahlen).
  • Neue Leads lösen wie gewohnt lead.created aus (siehe Webhooks).

Kunden auflisten

GET https://api.revenue-lab.de/v1/customers

Query-Parameter

ParameterTypBeschreibung
searchstringOptionalFreitextsuche in Name, Firma und E-Mail
updated_sincedatetimeOptionalNur Kunden, die seit diesem Zeitpunkt angelegt oder geändert wurden (wie beim Lead-Delta-Sync)
includestringOptionalcustom liefert je Kunde die Custom-Field-Werte mit
limitintegerOptionalMax. Ergebnisse (1–200, Standard: 50)
offsetintegerOptionalStartposition (Standard: 0)

Kunden-Objekt (Liste, Einzelabruf, PATCH und convert identisch): id, name, firma, branche, website, telefon, email, adresse, mitarbeiter, status, notizen, umsatz_pa (Zahl), lead_id, created_at, updated_at; bei Einzelabruf und PATCH zusätzlich custom.

200 OK
{
  "success": true,
  "data": [
    {
      "id":         12,
      "name":       "Max Mustermann",
      "firma":      "Mustermann GmbH",
      "email":      "max@mustermann-gmbh.de",
      "telefon":    "+49 221 1234567",
      "lead_id":    42,
      "created_at": "2026-06-05 15:00:00"
    },
    {
      "id":         13,
      "name":       "Lisa Schmidt",
      "firma":      "Schmidt & Partner AG",
      "email":      "l.schmidt@schmidt-partner.de",
      "telefon":    "+49 30 9876543",
      "lead_id":    43,
      "created_at": "2026-06-04 16:30:00"
    }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
cURL
curl "https://api.revenue-lab.de/v1/customers?search=Mustermann" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"
Python
import requests

API_KEY = "rl_live_DEIN_API_KEY"
BASE    = "https://api.revenue-lab.de/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}

res = requests.get(f"{BASE}/customers", headers=headers)
for k in res.json()["data"]:
    print(f"{k['id']}: {k['firma']} ({k['name']})")

Einzelnen Kunden abrufen

GET https://api.revenue-lab.de/v1/customers/{id}
cURL
curl "https://api.revenue-lab.de/v1/customers/12" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"
200 OK
{
  "success": true,
  "data": {
    "id":         12,
    "name":       "Max Mustermann",
    "firma":      "Mustermann GmbH",
    "email":      "max@mustermann-gmbh.de",
    "telefon":    "+49 221 1234567",
    "lead_id":    42,
    "created_at": "2026-06-05 15:00:00"
  }
}

Kunde löschen

Löscht einen Kunden endgültig samt Kontakten und Aktivitäten, wie „Kunde löschen“ im CRM. Der ursprüngliche Lead bleibt bestehen.

DELETE https://api.revenue-lab.de/v1/customers/{id}

Hat der Kunde Rechnungen oder Abos, antwortet die API mit 409 customer_has_billing und löscht nichts — bitte im CRM prüfen.

cURL
curl -X DELETE "https://api.revenue-lab.de/v1/customers/7" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"

ToDos auflisten

Listet alle ToDo-Karten des ToDo-Boards. Optional filterbar nach Spalte, zugewiesenem User oder Erledigt-Status.

GET https://api.revenue-lab.de/v1/todos

Query-Parameter

ParameterTypBeschreibung
column_idintNur Karten dieser Spalte (Spalten-IDs via /board sichtbar).
assigned_user_idintNur Karten, die diesem User zugewiesen sind.
completed0|11 = nur Karten in der letzten Spalte (Erledigt). 0 = alle anderen.
limitintMax 200 (Default 50).
offsetintPagination-Offset.
cURL
curl "https://api.revenue-lab.de/v1/todos?completed=0&assigned_user_id=5" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"

Einzelne ToDo abrufen

GET https://api.revenue-lab.de/v1/todos/{id}
200 OK
{
  "success": true,
  "data": {
    "id":                   "todo_42",
    "card_id":              42,
    "title":                "Lead nachfassen",
    "description":          "E-Mail-Sequenz Step 3",
    "color_label":          "#4a8fff",
    "due_date":             "2026-06-15",
    "assigned_user_id":     5,
    "assigned_user_name":   "Tom Weber",
    "column_id":            3,
    "column_name":          "In Arbeit",
    "is_completed":         false,
    "position":             2,
    "created_at":           "2026-06-08 11:22:00"
  }
}

ToDo anlegen

Erstellt eine neue Karte auf dem ToDo-Board. Loest den Webhook todo.created aus.

POST https://api.revenue-lab.de/v1/todos

Body (JSON)

FeldTypBeschreibung
title *stringKarten-Titel. Pflicht.
descriptionstringBeschreibung / Notiz.
column_idintOptional — ohne Angabe landet die Karte in der ersten Spalte.
color_labelstringHex-Farbe der Karte (z.B. #4a8fff). Default Blau.
due_datestringFaelligkeit (YYYY-MM-DD).
assigned_user_idintUser-ID, dem die Karte zugewiesen wird.
cURL
curl -X POST "https://api.revenue-lab.de/v1/todos" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Vertrag XY pruefen",
    "description": "Bis Freitag durchsehen und freigeben",
    "due_date": "2026-06-12",
    "color_label": "#ffb86b",
    "assigned_user_id": 5
  }'
201 Created
{
  "success": true,
  "data": {
    "id":                "todo_127",
    "card_id":           127,
    "title":             "Vertrag XY pruefen",
    "column_id":         1,
    "column_name":       "Backlog",
    "assigned_user_id":  5,
    "is_completed":      false
  }
}

ToDo aktualisieren

Aendert Felder einer Karte. Loest todo.updated aus. Wechselt die Karte in die letzte (Erledigt-)Spalte, wird zusaetzlich todo.completed gefeuert.

PATCH https://api.revenue-lab.de/v1/todos/{id}

Alle Felder optional. Felder die nicht im Body stehen, bleiben unveraendert.

cURL — Karte als erledigt markieren
curl -X PATCH "https://api.revenue-lab.de/v1/todos/127" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "column_id": 5 }'
Erledigt-Logik: Die letzte Spalte deines Boards (hoechste position) gilt als »Erledigt«. Verschiebst du eine Karte dorthin — egal ob per Drag im Board, per PATCH /v1/todos/{id} oder per Automation — feuert RevenueLab den Webhook todo.completed.

ToDo loeschen

DELETE https://api.revenue-lab.de/v1/todos/{id}
cURL
curl -X DELETE "https://api.revenue-lab.de/v1/todos/127" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"

Sign Lab (elektronische Unterschrift)

Dokumente zur einfachen elektronischen Signatur (eIDAS) senden, Status abfragen und das signierte PDF mit Prüfprotokoll abrufen. Unterzeichner brauchen kein Konto, sie bekommen einen persönlichen Link per E-Mail. Nach der letzten Unterschrift wird das PDF versiegelt und automatisch beim Lead bzw. Kunden abgelegt.

Scopes

ScopeErlaubt
sign:readGET auf /v1/sign-documents, /v1/sign-templates, /v1/sign-webhooks inkl. Download und Prüfprotokoll
sign:writePOST/DELETE: anlegen, senden, erinnern, zurückziehen, Webhooks verwalten

Die allgemeinen Scopes read/write/delete gelten für Sign Lab nicht. Bestehende Keys haben keinen Zugriff, bis ein Admin sign:read bzw. sign:write vergibt (Einstellungen → API-Zugang). Per API angelegte Dokumente werden ohne Admin-Freigabe versendet; die Freigabe ist nur bei KI-Agenten (MCP) Pflicht.

Zwei Versionen: Die Sign-API 2.0 unter /v1/sign/… (Documents, Recipients, Fields, Templates, Webhook-Subscriptions, Idempotenz) wird für neue Anbindungen empfohlen. Die Sign-API 1 unter /v1/sign-documents, /v1/sign-templates und /v1/sign-webhooks bleibt weiterhin verfügbar.

Status (Sign-API 1): draft · approval (KI-Anfrage wartet auf Freigabe) · sent · completed · declined · expired · cancelled. Unterzeichner: waiting · sent · viewed · signed · declined.

Sign-API 2.0

Basis https://api.revenue-lab.de/v1/sign, gleiche Scopes (sign:read für GET, sign:write für POST/PATCH/DELETE). Antworten sind die Objekte selbst ohne success/data-Hülle; Fehler: {"type":"request_error","code":"…","detail":"…"} mit HTTP 400/404/409/429. Versionskennung im Header X-RL-Sign-API: 2.0 und im Feld version.

StatusBedeutungFilter
document.draftEntwurf, kann bearbeitet und gesendet werden0
document.waiting_approvalvon einem KI-Agenten angelegt, wartet auf Admin-Freigabe6
document.sentversendet, noch niemand hat geöffnet1
document.viewedversendet, mindestens ein Empfänger hat geöffnet5
document.completedalle haben unterschrieben, PDF versiegelt2
document.declinedein Empfänger hat abgelehnt12
document.expiredFrist abgelaufenexpired
document.voidedvom Absender zurückgezogen11

Idempotenz: Header Idempotency-Key (8–100 Zeichen) bei POST/PATCH/DELETE. Dieselbe Anfrage innerhalb von 24 Stunden liefert die gespeicherte Antwort mit Idempotent-Replayed: true; derselbe Schlüssel mit anderem Inhalt ergibt 422 idempotency_key_reused, eine noch laufende Anfrage 409 idempotency_in_progress. Seiten: page (ab 1) und count (Standard 50, höchstens 100); Antwort {"results":[…],"page","count","total","has_more"}.

Documents

GEThttps://api.revenue-lab.de/v1/sign/documents?status=document.sent&q=&template_id=&lead_id=&kunde_id=&order_by=-date_created&page=1&count=50
POSThttps://api.revenue-lab.de/v1/sign/documents
GEThttps://api.revenue-lab.de/v1/sign/documents/{id}
GEThttps://api.revenue-lab.de/v1/sign/documents/{id}/details
POSThttps://api.revenue-lab.de/v1/sign/documents/{id}/send
POSThttps://api.revenue-lab.de/v1/sign/documents/{id}/send-reminder
POSThttps://api.revenue-lab.de/v1/sign/documents/{id}/void
DELETEhttps://api.revenue-lab.de/v1/sign/documents/{id}
GEThttps://api.revenue-lab.de/v1/sign/documents/{id}/download · /download-protected · /audit-trail

{id} ist die Zahl aus id oder die 16-stellige uuid. GET …/{id} liefert das Status-Objekt (id, uuid, name, status, date_created, date_modified, date_sent, date_completed, expiration_date, ref_number, version), /details zusätzlich recipients (id, first_name, last_name, email, recipient_type, role, signing_order, has_completed, status, signature_date), fields (field_id, name, type, value, is_filled, assigned_to, page, position in Prozent), metadata, template, created_by, sent_by und Prüfsummen. /download liefert das aktuelle PDF (versiegelt, sonst Original; Header X-RL-Document-State), /download-protected nur das versiegelte (sonst 409). DELETE nur für Entwürfe, laufende Dokumente mit /void zurückziehen.

Feld (POST)TypBeschreibung
namestringTitel des Dokuments
template_uuidstringVorlage (ID aus /v1/sign/templates); sonst file
filestringPDF als Base64 (max. 20 MB). Ein Abruf per url wird aus Sicherheitsgründen nicht unterstützt.
recipientsarray[{"email","first_name","last_name","role","signing_order"}]. Vorlage: Zuordnung über role (Rollenname), sonst nach Reihenfolge. PDF: signing_order bei allen = nacheinander (serverseitig erzwungen), bei keinem = gleichzeitig.
tokensarray[{"name":"company","value":"…"}] füllt bzw. überschreibt Platzhalter der Vorlage
fieldsobject | arrayVorlage: {"Ort":{"value":"Hamburg"}} belegt Textfelder nach Beschriftung vor (Unterschriften nicht). PDF: [{"type":"signature|date|text|checkbox","page":1,"position":{"x":10,"y":80,"width":30,"height":6},"assigned_to":"Kunde","required":true,"title":"…","value":"","locked":false}]; assigned_to = Rolle, E-Mail oder Index in recipients. Bei checkbox ist title der Text neben dem Kästchen (mehrzeilig, Links als [Text](https://…)).
metadataobject{"lead_id":128} oder {"kunde_id":7}: Platzhalter aus dem Kontakt, Ablage des signierten PDFs
message, expiration_days, reminder_daysmixedNachricht in der Einladung, Gültigkeit (1–365), Erinnerung alle n Tage (0 = aus)

Die Antwort (201) ist das Status-Objekt mit status: "document.draft"; fehlt für den Versand noch etwas, steht es in info_message. Danach POST …/send (optional {"message":"…"}; silent wird nicht unterstützt, weil der persönliche Link nur per E-Mail geht).

cURL
curl -X POST "https://api.revenue-lab.de/v1/sign/documents" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: angebot-128-v1" \
  -d '{"name":"Rahmenvertrag","template_uuid":"3","metadata":{"lead_id":128},
       "recipients":[{"email":"kunde@example.com","first_name":"Erika","last_name":"Beispiel","role":"Kunde"}],
       "tokens":[{"name":"company","value":"Beispiel GmbH"}]}'

curl -X POST "https://api.revenue-lab.de/v1/sign/documents/42/send" -H "Authorization: Bearer rl_live_DEIN_API_KEY" -H "Idempotency-Key: angebot-128-send"

Templates

GEThttps://api.revenue-lab.de/v1/sign/templates?q=&page=1&count=50
GEThttps://api.revenue-lab.de/v1/sign/templates/{id}/details

/details liefert roles (name, signing_order, source: contact | sender | fixed), tokens (verfügbare Platzhalter) und fields mit assigned_to.role.

Webhook-Subscriptions und -Events

POSThttps://api.revenue-lab.de/v1/sign/webhook-subscriptions
GEThttps://api.revenue-lab.de/v1/sign/webhook-subscriptions[/{uuid}]
PATCHhttps://api.revenue-lab.de/v1/sign/webhook-subscriptions/{uuid} · /shared-key
DELETEhttps://api.revenue-lab.de/v1/sign/webhook-subscriptions/{uuid}
GEThttps://api.revenue-lab.de/v1/sign/webhook-events?since=&to=&type=sign.signed&status=failed[/{uuid}]

Body {"name":"CRM","url":"https://…","triggers":["sign.sent","sign.viewed","sign.signed","sign.completed","sign.declined"],"active":true}. Auslöser: sign.sent (versendet), sign.viewed (geöffnet), sign.signed (ein Empfänger hat unterschrieben), sign.completed (alle, PDF versiegelt), sign.declined; alternativ die Ereignisnamen document_state_changed, recipient_completed und document_completed_pdf_ready. Die Antwort enthält einmalig shared_key; PATCH …/shared-key erzeugt einen neuen.

Die Nutzlast ist eine Liste von Ereignissen: [{"event":"sign.signed","trigger":"recipient_completed","event_id":"evt_…","created_at":"…","data":{"document":{…,"recipients":[…]},"recipient":{…},"action_by":{…},"action_date":"…"}}], bei sign.completed zusätzlich data.download_url. Die Signatur steht (wie bei Sign-API 1) im Header X-RL-Signature: t=<unix>,v1=<hex> mit HMAC-SHA256(shared_key, "<unix>.<roher Body>"). /webhook-events listet die Zustellungen (uuid, type, status, http_status_code, error, attempts, delivery_time), die Einzelansicht zusätzlich payload.

Dokumente auflisten und abrufen

GEThttps://api.revenue-lab.de/v1/sign-documents?status=sent&lead_id={id}
GEThttps://api.revenue-lab.de/v1/sign-documents/{id}
GEThttps://api.revenue-lab.de/v1/sign-documents/{id}/events
GEThttps://api.revenue-lab.de/v1/sign-documents/{id}/download

/events liefert das Prüfprotokoll (Ereignisse mit IP und Hash-Kette, chain.ok). /download liefert das signierte PDF (binär, application/pdf), mit ?kind=original das unveränderte Original. sealed_sha256 ist die Prüfsumme des signierten PDFs.

cURL
curl -o vertrag.pdf "https://api.revenue-lab.de/v1/sign-documents/42/download" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY"

Dokument anlegen und senden

POSThttps://api.revenue-lab.de/v1/sign-documents

Variante 1 aus Vorlage (IDs über GET /v1/sign-templates): Platzhalter wie {{first_name}} oder {{company}} werden aus dem Lead bzw. Kunden gefüllt, die Unterzeichner kommen aus den Rollen der Vorlage. Variante 2 eigenes PDF per file_base64 mit Unterzeichnern und Feldern (Position in Prozent der Seite, oben links).

FeldTypBeschreibung
template_idintVorlage (Variante 1)
lead_id / kunde_idintKontakt: füllt Platzhalter, das signierte PDF wird dort abgelegt
signersarray[{"name","email","role"}] in Reihenfolge; bei Vorlagen überschreibt es Name/E-Mail je Rolle
file_base64, filename, titlestringPDF (max. 20 MB, ohne Passwortschutz und Skripte), Variante 2
fieldsarray{"type":"signature|date|text|checkbox","page":1,"x":10,"y":80,"w":30,"h":6,"signer_index":1,"required":true,"label":"","prefill":"","locked":false}
signing_orderstringsequential (Standard, serverseitig erzwungen) oder parallel
message, expires_days, reminder_daysmixedNachricht in der Einladung, Gültigkeit (1–365 Tage), Erinnerung alle n Tage (0 = aus, höchstens 3)
sendbooltrue = sofort versenden, sonst bleibt es ein Entwurf. Fehlt etwas, steht es in problems.
cURL
curl -X POST "https://api.revenue-lab.de/v1/sign-documents" \
  -H "Authorization: Bearer rl_live_DEIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"template_id": 3, "lead_id": 128, "send": true}'

Senden, erinnern, zurückziehen

POSThttps://api.revenue-lab.de/v1/sign-documents/{id}/send
POSThttps://api.revenue-lab.de/v1/sign-documents/{id}/remind
POSThttps://api.revenue-lab.de/v1/sign-documents/{id}/cancel

remind nimmt optional signer_id (sonst der erste offene Unterzeichner; höchstens eine Erinnerung pro Stunde), cancel optional reason. Jede Erinnerung erzeugt einen neuen persönlichen Link, der vorige wird ungültig.

Sign-Lab-Webhooks (HMAC-signiert)

POSThttps://api.revenue-lab.de/v1/sign-webhooks

Body {"url":"https://…","events":["sign.completed","sign.declined","sign.viewed"]}. Die Antwort enthält einmalig das Geheimnis (secret). Auflisten mit GET, entfernen mit DELETE /v1/sign-webhooks/{id}. Nur https-Ziele, keine internen Adressen.

Jede Zustellung ist ein POST mit JSON (id, event, created_at, data.document, ggf. data.signer, bei sign.completed zusätzlich data.download) und den Headern X-RL-Event, X-RL-Delivery, X-RL-Timestamp und X-RL-Signature: t=<unix>,v1=<hex>. Prüfung: HMAC-SHA256(secret, "<unix>.<roher Body>") vergleichen und nur Zeitstempel jünger als 5 Minuten akzeptieren. Antwortet das Ziel nicht mit 2xx, wird nach 1, 5 und 30 Minuten sowie 2 und 12 Stunden erneut zugestellt.

PHP
$body = file_get_contents('php://input');
[$t, $v] = sscanf($_SERVER['HTTP_X_RL_SIGNATURE'], 't=%d,v1=%s');
$ok = hash_equals(hash_hmac('sha256', $t . '.' . $body, $secret), (string)$v) && abs(time() - $t) < 300;

KI-Anbindung (MCP): Lead-Portal

POSThttps://dashboard.revenue-lab.de/mcp

Der MCP-Server (Model Context Protocol, JSON-RPC über HTTP) ist ein eigener Zugang für KI-Assistenten wie Claude, ChatGPT oder Cursor und nicht Teil der REST-API: Schlüssel rlm_… (Einstellungen → KI-Anbindung), nicht rl_live_…. Für das Lead-Portal gibt es fünf Werkzeuge. Fachliche Erklärung mit Bildern: Wiki: Reklamationen und Abrechnung per KI-Agent.

Zugang und Rechte

  • Beim Anlegen des Zugangs das Modul Lead-Portal wählen. Zugänge ohne ausdrückliche Modulwahl (ältere) haben darauf keinen Zugriff.
  • read für list_claims, get_claim, get_order_figures; send für propose_claim_decision und cancel_claim_proposal.
  • Entschieden wird nie per MCP. Ein Vorschlag löst eine Glocken-Meldung bei allen Admins aus und erscheint im Lead-Portal am Fall; erst „Anerkennen“ oder „Ablehnen“ eines Menschen gilt und schließt den Vorschlag.

Aufruf

HTTP
POST /mcp
Authorization: Bearer rlm_DEIN_MCP_SCHLUESSEL
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"list_claims","arguments":{"status":"offen"}}}

Die Antwort steht als JSON-Text in result.content[0].text. Fehler eines Werkzeugs kommen mit result.isError = true und lesbarem Text.

list_claims — Reklamationen lesen

ArgumentTypBeschreibung
statusstringoffen (Standard), akzeptiert, abgelehnt oder alle
project_idintegerNur dieser Auftrag
limitintegerStandard 50, höchstens 200

get_claim mit lead_id liefert denselben Eintrag für einen Lead. Ein Eintrag:

JSON
{
  "lead_id": 3, "kind": "lead",
  "name": "Max Muster", "company": "Beispiel GmbH",
  "project_id": 1, "project_name": "Lead-Lieferung Oktober",
  "client": { "id": 1, "name": "Muster AG" },
  "delivered_at": "2026-10-03 08:38:44",
  "claim": {
    "status": "offen", "reason": "Nicht erreichbar",
    "note": "Nummer klingelt ins Leere", "claimed_at": "2026-10-20 11:06:31",
    "decided_at": null, "decision_note": null, "claim_days": 14
  },
  "counts_for_billing": true, "not_counted_reason": null,
  "feedback": { "status": "kontaktiert", "status_at": "2026-10-06 08:39:16", "note": "3x versucht" },
  "ai_proposal": { "id": 3, "decision": "akzeptiert", "note": "Grund plausibel", "by": "Claude Vertrieb", "at": "2026-10-20 11:06:32", "status": "pending" }
}
  • kind: lead oder termin. Bei Terminen ist delivered_at die Buchung und feedback enthält outcome, happened_at, noshow_at, termin_at, notes.
  • counts_for_billing ist false, wenn der Lead nicht zählt (Bad Data, vor Lieferbeginn); der Grund steht in not_counted_reason.

propose_claim_decision — Entscheidung vorschlagen

ArgumentTypBeschreibung
lead_idintegerLead mit offener Reklamation (sonst Fehler)
decisionstringakzeptiert (anerkennen) oder abgelehnt
notestringBegründung für die Admins, bis 500 Zeichen, nur intern (der Auftraggeber sieht sie nie)

Antwort: {"proposal_id":3,"lead_id":3,"decision":"akzeptiert","status":"pending","message":"Die Reklamation wurde NICHT entschieden. …"}. Ein neuer Vorschlag ersetzt den bisherigen offenen; höchstens 30 Vorschläge je Zugang und Stunde. cancel_claim_proposal (lead_id) zieht den offenen Vorschlag zurück. Zieht der Auftraggeber die Reklamation zurück, erledigt sich der Vorschlag von selbst.

get_order_figures — Zahlen für die Abrechnung

ArgumentTypBeschreibung
project_idintegerNur dieser Auftrag; ohne Angabe alle Lead- und Termin-Aufträge
fromstringErster Tag, z. B. 2026-10-12
tostringLetzter Tag (einschließlich), z. B. 2026-10-18
JSON
{
  "period": { "from": "2026-10-12", "to": "2026-10-18" },
  "orders": [{
    "project_id": 1, "name": "Lead-Lieferung Oktober", "type": "leads",
    "client": { "id": 1, "name": "Muster AG" },
    "price_per_unit": 40, "unit": "Lead",
    "quota": 50, "quota_invoice_ref": "RE-2026-0142",
    "quota_used_total": 17, "quota_remaining": 33,
    "counters": { "delivered": 3, "answered": 2, "won": 0, "lost": 0 },
    "claims": { "open": 1, "accepted": 2, "rejected": 0 },
    "billable": 1, "amount": 40,
    "open_claims_note": "Es gibt noch offene Reklamationen: der Betrag kann sich bei Anerkennung verringern."
  }],
  "total": { "billable": 1, "amount": 40 }
}
  • billable = geliefert minus anerkannte Reklamationen; amount = billable × price_per_unit (Preis je Lead bzw. je Termin).
  • Zurückgehaltene Leads (über dem Kontingent) und Bad Data zählen nicht; Leads zählen ab dem Lieferbeginn des Auftrags.
  • Das Kontingent (quota*, nur bei Lead-Aufträgen) bezieht sich auf die gesamte Lieferung, nicht auf den Zeitraum.

Automationen — Outbound Webhooks

Die REST-API schreibt Daten in RevenueLab. Automationen senden Events heraus: Tritt ein Ereignis ein (z.B. Pipeline-Stage aendert sich), feuert RevenueLab einen POST-Request an deine Ziel-URL.

Einrichtung: Automationen unter Settings → Automationen verwalten (nur Admin). Pro Regel: Name, Trigger-Ereignis, optionaler Stage-Filter, Ziel-URL. Mit dem Test-Button wird ein Beispiel-Payload sofort gesendet.

Trigger-Ereignisse

EventWannAction im Payload
lead.created Ein Lead wird angelegt (CRM oder API). created
pipeline.update Die Pipeline-Stage eines Leads aendert sich. Optional auf eine Ziel-Stage einschraenkbar (z.B. nur Gewonnen). updated
appointment.set Ein Termin wird gesetzt (termin_at) oder der Status wechselt auf Terminiert. updated
todo.created Eine neue ToDo-Karte wird angelegt (Board oder API). created
todo.updated Eine ToDo-Karte wird geaendert (Titel, Beschreibung, Faelligkeit, Zuweisung, Spalte). updated
todo.completed Eine ToDo-Karte wird in die letzte Spalte (Erledigt) verschoben — egal ob via Board-Drag oder API-PATCH. completed
Zustellung: Fire-and-forget mit 3 Sekunden Timeout. Antworte mit HTTP 2xx um Empfang zu bestaetigen. Kein automatischer Retry — ist dein Endpunkt offline, geht das Event verloren. Die UI wird nicht blockiert (Response wird vor dem Webhook abgeschlossen).

Webhook-Payload

Jeder Webhook sendet ein einheitliches Envelope (Close-Stil). Geaenderte Felder werden als changed_fields mitgeliefert, inklusive vorheriger Werte in previous_data. Custom Fields liegen unter data.custom.*.

JSON — Beispiel: pipeline.update
{
  "subscription_id": "whsub_3",
  "tenant_id": 1,
  "event": {
    "id":             "ev_k7Bm3xQ9pLrT2vN8cZ1a4",
    "date_created":   "2026-06-05T14:22:00+02:00",
    "object_type":    "lead",
    "object_id":      "lead_42",
    "lead_id":        42,
    "action":         "updated",
    "trigger":        "pipeline.update",
    "changed_fields": ["status", "pipeline_stage"],
    "data": {
      "id":             42,
      "name":           "Max Mustermann",
      "company":        "Mustermann GmbH",
      "email":          "max@mustermann-gmbh.de",
      "phone":          "+49 221 1234567",
      "status":         "Gewonnen",
      "pipeline_stage": "Gewonnen",
      "deal_value":     "250.00",
      "termin_at":      "2026-06-10 14:00:00",
      "project_name":   "Outbound B2B DACH",
      "custom": {
        "branche":     "IT-Dienstleistung",
        "mitarbeiter": "25"
      }
    },
    "previous_data": {
      "status":         "Terminiert",
      "pipeline_stage": "Terminiert"
    },
    "actor": {
      "id":   7,
      "name": "Tom Weber"
    }
  }
}

Envelope-Felder

FeldTypBeschreibung
subscription_idstringID der Automation. Prefix: whsub_.
tenant_idintegerDein Account / Mandant.
event.idstringEindeutige Event-ID, Prefix ev_. Nutzbar zur Deduplizierung / Idempotenz.
event.date_createdstringISO-8601 Timestamp mit Zeitzone.
event.object_typestringAktuell immer "lead".
event.object_idstringSprechende ID, z.B. "lead_42". Numerisch auch als event.lead_id.
event.actionstring"created" oder "updated".
event.triggerstringDas ausloesende Ereignis: lead.created, pipeline.update, appointment.set.
event.changed_fieldsstring[]Liste der geaenderten Feldnamen. Bei created: alle befuellten Felder.
event.dataobjectVollstaendiger aktueller Lead-Zustand. Custom Fields unter data.custom.<key>.
event.previous_dataobjectAlte Werte der geaenderten Felder (Diff). Bei created leer: {}.
event.actorobjectAusloesender Nutzer mit id und name.
n8n / Make Tipp: Filtere auf event.changed_fields (z.B. enthaelt "pipeline_stage"), lies den neuen Wert aus event.data.pipeline_stage und den alten aus event.previous_data.pipeline_stage. So reagierst du gezielt auf bestimmte Aenderungen.

Event-Log

Protokoll aller eingehenden API-Calls und ausgehenden Webhook-Zustellungen. Gleiche Daten wie unter Settings → Event-Log.

GET https://api.revenue-lab.de/v1/events

Query-Parameter

ParameterTypBeschreibung
categorystringapi (eingehende Calls) oder webhook (ausgehende Zustellungen). Leer = beides.
limitintegerMax. Ergebnisse (1–200, Standard: 50)
offsetintegerStartposition (Standard: 0)
200 OK
{
  "success": true,
  "data": [
    {
      "id":          501,
      "category":    "api",
      "method":      "POST",
      "route":       "/v1/leads",
      "object_type": "lead",
      "object_id":   "42",
      "status_code": 201,
      "success":     true,
      "actor":       "API Key: rl_live_abc1...",
      "ip":          "203.0.113.42",
      "created_at":  "2026-06-05 10:30:00"
    },
    {
      "id":          502,
      "category":    "webhook",
      "method":      "lead.created",
      "route":       "https://hook.eu2.make.com/abc123...",
      "object_type": "lead",
      "object_id":   "42",
      "status_code": 200,
      "success":     true,
      "actor":       "Tom Weber",
      "meta":        { "changed_fields": ["name", "company", "phone"] },
      "created_at":  "2026-06-05 10:30:01"
    }
  ],
  "total": 502,
  "limit": 50,
  "offset": 0
}

Wie API-Calls funktionieren

Diese Sektion erklärt das mentale Modell der API — gut zu lesen bevor du den ersten Workflow baust. Spart später viel Debugging.

1. Lebenszyklus eines Leads

Ein Lead durchläuft in RevenueLab folgende Stufen — jede davon kannst du per API steuern:

PhaseWer triggertAPI-Call
ErstellungExterne Quelle (Website-Formular, Werbe-Lead, Import)POST /v1/leads
Kontakt & PflegeAgent im Dialer + externe Tools (E-Mail-Antwort, SMS)PATCH /v1/leads/{id} + POST /v1/leads/{id}/notes
Angebot/VertragVertragstool (PandaDoc, Documenso) via n8nPOST /v1/leads/{id}/files mit as_contract=1
AbschlussVertragstool / Sales-AgentPATCH /v1/leads/{id} mit status=Gewonnen → Auto-Logik feuert
KundenpflegeCRM-Team (eigene Kunden-API, oder via Customer-ID)GET /v1/customers/{id}

2. Der "PATCH-Geist": send only what changes

Anders als REST-PUT überschreibt PATCH den Lead nicht mit dem Body. Es ändert nur die Felder, die du explizit mitschickst. Felder die nicht im Body sind, bleiben unangetastet.

Häufiger Anfänger-Fehler: Bei PATCH alle Felder mitsenden "weil man sie hat". Resultat: du überschreibst Felder die ein Sales-Agent gerade manuell geändert hat. Schick nur das, was du wirklich ändern willst. null = bewusst leer setzen. Feld weglassen = unverändert lassen.

3. Idempotenz — ein API-Call darf doppelt passieren

Webhook-Provider (n8n, Make, Zapier) wiederholen Calls bei Timeouts. Die wichtigsten RevenueLab-Endpoints sind darauf vorbereitet:

  • POST /v1/leads/{id}/convert — legt den Kunden nur an wenn noch keiner existiert, sonst gibt es den bestehenden zurück (created: false).
  • PATCH /v1/leads/{id} mit status=Gewonnen — Auto-Convert prüft ob schon ein Kunde da ist und legt nur fehlende an.
  • POST /v1/leads/{id}/files mit as_contract=1 — jeder Upload erzeugt einen eigenen Datei- und Vertrags-Eintrag (kein Dedup auf Filename), das ist gewollt.

4. Fire-and-Forget bei Webhooks

Wenn ein PATCH einen Outbound-Webhook auslöst (pipeline.update, appointment.set), wartet die API nicht auf die Webhook-Zustellung. Du bekommst die 200 OK-Antwort sofort, der Webhook geht im Hintergrund raus (fastcgi_finish_request). Vorteil: keine Verzögerung in deinem Workflow. Nachteil: wenn der Empfänger 500 zurückgibt, siehst du das nur im Event-Log (category=webhook).

5. Was du beobachtest, ist nicht die Wahrheit

Der Webhook-Payload enthält previous_data (was vorher war) und changed_fields (was sich geändert hat). Wenn dein Workflow auf "Lead wurde Gewonnen" reagieren soll, prüfe "pipeline_stage" in changed_fields && data.pipeline_stage === "Gewonnen" — nicht nur data.pipeline_stage === "Gewonnen". Sonst feuerst du bei jedem Update eines Won-Leads erneut.

Auto-Logik bei "Gewonnen"

Sobald ein Lead in eine is_won-Pipeline-Stage geht (standardmäßig Gewonnen, plus jede Custom-Stage mit is_won=1), feuert RevenueLab eine Kaskade von Auto-Aktionen. Das gilt für jeden Weg dorthin: UI, PATCH, File-Upload, oder Documenso-Webhook.

Was automatisch passiert

#AktionBedingung
1Kunde wird angelegt (Tabelle kunden) — mit Name, Firma, Telefon, E-Mail, Website, Adresse und Umsatz (aus deal_value).Nur wenn noch kein Kunde mit diesem lead_id existiert (idempotent).
2Kunden-Aktivität "system" angelegt — sichtbar im Kunden-Drawer.Immer, wenn (1) feuert.
3status wird auf Gewonnen gesetzt (auch bei Custom-is_won-Stages).Nur wenn Client nicht explizit anderen Status mitgab.
4deal_probability wird auf 100 gesetzt.Nur wenn Client keinen expliziten Wert mitgab. Überschreibt bestehende Werte (z. B. 70% → 100), denn "Gewonnen mit 70%" ist semantisch sinnlos.
5expected_close_at wird auf heute gesetzt.Nur wenn Client keinen expliziten Wert mitgab UND DB-Wert vorher leer war. Überschreibt KEINE bestehenden Werte — ein gesetztes Forecast-Datum bleibt.
6Audit-Eintrag wird angelegt (vertrag_unterzeichnet bei Datei-Upload als Vertrag, sonst pipeline_stage-Änderung).Immer.
7Outbound-Webhook pipeline.update wird gefeuert (Close-Style mit changed_fields + previous_data).Wenn aktive Automation für diesen Trigger existiert.

Begründung der Asymmetrie zwischen Punkt 4 und 5

Wahrscheinlichkeit ist axiomatisch — ein gewonnener Lead hat per Definition 100%. "Gewonnen mit 70%" widerspricht sich selbst, deshalb wird der Wert überschrieben.

Close-Date dagegen kann legitim in der Zukunft liegen ("Vertrag tritt am 01.09.2026 in Kraft" / "Kampagnenstart erst nächsten Monat"). Wenn Sales das bewusst gesetzt hat, würden wir Daten zerstören, indem wir auf "heute" überschreiben. Darum: nur befüllen wenn leer.

Was du explizit überschreiben kannst

Jeder der oben automatisierten Werte lässt sich vom Client immer explizit setzen — explizite Werte schlagen Auto-Defaults:

Override-Beispiel
// Beispiel: Lead gewinnen, aber mit nur 90% Wahrscheinlichkeit
// (z.B. weil Anzahlung noch offen ist) und Close-Date in der Zukunft
{
  "status":            "Gewonnen",
  "deal_probability":  90,
  "expected_close_at": "2026-09-01"
}

Die drei Wege zu "Gewonnen"

WegCallWann nutzen
PATCH statusPATCH /v1/leads/{id}
{ "status": "Gewonnen" }
Einfachster Weg. Wenn dein Workflow eh PATCH-Felder updatet (Deal-Wert, Notiz, Custom-Felder).
PATCH pipeline_stagePATCH /v1/leads/{id}
{ "pipeline_stage": "Premium-Won" }
Wenn du eine Custom-Stage mit is_won=1 nutzt (z. B. um nach Deal-Größe zu segmentieren).
File-Upload as_contractPOST /v1/leads/{id}/files
multipart mit as_contract=1
Wenn du eine unterzeichnete Vertrags-PDF hochlädst (PandaDoc, DocuSign). Datei landet im Verträge-Tab UND Lead geht auf Gewonnen, sofern auto_won_on_contract_signed aktiv ist.
Wo das Toggle sitzt: Settings → Documenso → "Lead automatisch auf Gewonnen wenn Vertrag unterzeichnet". Steuert ob File-Upload as_contract=1 die ganze Won-Kaskade triggert oder nur den Vertrag ablegt.

Custom Fields

Custom Fields ("Benutzerdefinierte Felder") sind tenant-spezifische Zusatzfelder, die du im Admin-Bereich definierst (Name, Typ, Sichtbarkeitsbereich). RevenueLab speichert sie getrennt von der leads-Tabelle in custom_field_values — du musst sie aber API-seitig genauso behandeln wie normale Felder.

Lesen: bei jedem Lead-Read enthalten

Sowohl GET /v1/leads/{id} als auch der Webhook-Payload (data.custom) enthalten alle Custom-Field-Werte als { field_key: value }:

Auszug Lead-Response
{
  "data": {
    "id":    4426,
    "name":  "Sabine Jäger",
    // ... Standard-Felder ...
    "custom": {
      "angebotspaket":        "LinkedIn Starter",
      "garantiertetermine":   "5",
      "kampagnenstart":       "2026-07-01",
      "laufzeit":             "9 Monate",
      "preisnetto":           "12500"
    }
  }
}

Schreiben: custom-Objekt im PATCH-Body

Im PATCH-Body fügst du ein custom-Objekt hinzu. Die Keys sind die field_key-Werte (genauso wie im Read-Payload). Werte: String/Number/Boolean. null löscht den Wert.

PATCH mit Custom-Feldern
PATCH /v1/leads/4426
{
  "deal_value": 12500,
  "custom": {
    "angebotspaket":  "LinkedIn Premium",
    "kampagnenstart": "2026-07-15",
    "preisnetto":     "18500"
  }
}

Die Response enthält custom_saved: [...] mit der Liste der tatsächlich gespeicherten Keys — perfekt zum Debugging:

200 OK
{
  "success": true,
  "data": { /* ... inkl. custom-Objekt mit neuen Werten ... */ },
  "custom_saved": ["angebotspaket", "kampagnenstart", "preisnetto"]
}
Unbekannte Keys werden silent ignoriert. Schickst du "angbotspaket" (Tippfehler) statt "angebotspaket", erscheint der Key NICHT in custom_saved — aber es gibt auch keinen Fehler. Das ist gewollt (Tenants können Felder löschen, ohne dass alte Integrationen brechen), erfordert aber dass du custom_saved in deinem Workflow prüfst wenn du sicher gehen willst.

Wo finde ich die field_keys?

  • Admin → Custom Fields → Spalte "Key"
  • Oder: einmal GET /v1/leads/{id} aufrufen — im custom-Objekt siehst du alle Keys
  • Oder: einen Webhook für lead.created oder pipeline.update anlegen — im Payload steht event.data.custom mit allen Keys

Lead-Status-Werte

Erlaubte Werte fuer das Feld status bei POST /v1/leads und PATCH /v1/leads/{id}:

StatusBeschreibungPipeline-StageIn Dialer-Queue?
NeuNeuer Lead, noch nicht kontaktiert (Standard)In AuftragJa
WiedervorlageRueckruf geplant — erscheint zum Wiedervorlage-ZeitpunktIn AuftragJa
Nicht erreichtAnrufversuch ohne ErreichungIn AuftragJa
Entscheider nicht erreichtNur Sekretariat/Assistent erreichtIn AuftragJa
TerminiertTermin wurde vereinbartIn AuftragNein
Kein InteresseLead hat abgelehntIn AuftragNein
Bad DataFehlerhafte KontaktdatenBad DataNein
Info Mail angefordertInfomaterial angefordertIn AuftragNein
Info Mail versendetInfomaterial wurde versendetIn AuftragNein
GewonnenDeal abgeschlossen — löst Auto-Convert-Kaskade ausGewonnenNein
VerlorenDeal verloren (Grund optional via lost_reason)VerlorenNein

Zusätzlich gelten die eigenen Status deines Accounts (CRM → Einstellungen → Lead-Status). Vom Account archivierte Status werden abgelehnt. Bei einem ungültigen Wert nennt die Fehlerantwort invalid_status alle aktuell erlaubten Werte. Die Liste ist für POST und PATCH identisch.

Lead-Objekt (vollstaendiges Schema)

Jedes Lead-Objekt in data enthaelt diese Felder:

FeldTypBeschreibung
idintegerEindeutige Lead-ID
project_idintegerZugehoeriges Projekt
project_namestringProjektname (read-only, JOIN)
namestring|nullVor- und Nachname
salutation / first_name / last_namestring|nullAufgespaltene Namen
companystring|nullFirmenname
legal_formstring|nullRechtsform
positionstring|nullPosition / Jobtitel
lead_typestringb2b oder b2c
phonestring|nullTelefonnummer
emailstring|nullE-Mail-Adresse
websitestring|nullWebsite-URL
addressstring|nullAdresse als ganzer String (Legacy)
street / zip / city / countrystring|nullAufgespaltene Adressfelder
statusstringLead-Status (siehe Status-Werte)
pipeline_stagestringAktuelle Pipeline-Stage (Standard oder Custom)
lost_reasonstring|nullVerlust-Grund (nur bei status=Verloren)
sourcestring|nullLead-Quelle
deal_valuenumber|nullDeal-Wert in Euro
deal_probabilityint|nullAbschlusswahrscheinlichkeit (0–100). Bei "Gewonnen" auto auf 100.
expected_close_atdate|nullVoraussichtl. Abschluss-Datum (YYYY-MM-DD)
termin_atdatetime|nullVereinbarter Termin mit dem Lead
appointment_set_atdatetime|nullZeitpunkt der Terminvereinbarung
followup_atdatetime|nullWiedervorlage-Zeitpunkt
notesstring|nullNotizen-Text (Single-Field — für Verlauf: /v1/leads/{id}/notes)
customobjectCustom-Field-Werte (siehe Custom Fields)
external_refstring|nullDein Fremd-Schlüssel (siehe Upsert); über GET /v1/leads?external_ref=… abrufbar
source_notestring|nullZusatzinfo zur Herkunft
termin_notesstring|nullNotizen zum vereinbarten Termin
created_atdatetimeErstellungszeitpunkt
updated_atdatetimeLetzter Aenderungszeitpunkt (Serverzeit Europe/Berlin), wird bei jeder Änderung automatisch gesetzt

Einheitliche Form: GET (Liste und Einzelabruf), POST, PATCH und der Lead-Abruf nach convert liefern dasselbe Objekt. Felder ohne Wert sind null (nie weggelassen), Zahlen wie deal_value sind JSON-Zahlen. custom ist bei Einzelabruf, POST und PATCH enthalten, in Listen nur mit ?include=custom. Interne Felder (z.B. tenant_id) werden nicht ausgegeben.

Zapier, Make & n8n Integration

n8n — HTTP Request Node

  1. Node: HTTP Request
  2. Method: POST
  3. URL: https://api.revenue-lab.de/v1/leads
  4. Authentication: Header Auth
  5. Header Name: Authorization — Value: Bearer rl_live_DEIN_API_KEY
  6. Body Type: JSON
  7. Body: Felder mappen (project_id, name, phone, email, ...)

n8n — Webhook empfangen

  1. Node: Webhook (Trigger)
  2. HTTP Method: POST
  3. Webhook-URL kopieren und in Settings → Automationen als Ziel-URL eintragen
  4. Daten liegen unter body.event.data.* und body.event.changed_fields

Make (Integromat) — HTTP-Modul

  1. Modul: HTTP → Make a request
  2. URL: https://api.revenue-lab.de/v1/leads
  3. Method: POST
  4. Headers: Authorization → Bearer rl_live_DEIN_API_KEY
  5. Body type: Raw → application/json

Zapier — Webhooks by Zapier

  1. Trigger: Beliebig (Typeform, Jotform, Google Forms, ...)
  2. Action: Webhooks by Zapier → POST
  3. URL: https://api.revenue-lab.de/v1/leads
  4. Payload Type: json
  5. Headers: Authorization | Bearer rl_live_DEIN_API_KEY
JSON — Zapier / Make / n8n Data Mapping
{
  "project_id": 1,
  "name":       "{{Name}}",
  "email":      "{{Email}}",
  "phone":      "{{Telefon}}",
  "company":    "{{Firma}}",
  "notes":      "Quelle: {{Formularname}}"
}
Projekt-ID herausfinden: Einmalig GET /v1/projects aufrufen, die gewuenschte ID notieren und im Workflow fest hinterlegen.