Übersicht
Authorization: Bearer rl_live_... → GET https://dashboard.revenue-lab.de/api?__path=projects aufrufen → fertig.
https://api.revenue-lab.de/v1Alle 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_idbei Lead-Erstellung) - Event-Log aller API-Calls und Webhook-Zustellungen abfragen
- Outbound Webhooks — bei Events automatisch Daten an Make / n8n / Zapier pushen
REST API (Bearer-Token) = Daten in RevenueLab schreiben.
Webhooks (Automationen) = Events aus RevenueLab heraus an deine Systeme senden.
Authentifizierung
Jeder Request braucht einen Bearer-Token im Authorization-Header:
Authorization: Bearer rl_live_abc123def456ghi789jkl012mno345pqrs6789
API-Key Regeln
| Eigenschaft | Wert |
|---|---|
| Format | rl_live_ + 40 Hex-Zeichen = 48 Zeichen total |
| Verwaltung | Settings → API-Zugang |
| Sichtbarkeit | Nur einmalig nach Erstellung im Klartext sichtbar |
| Limit | Max. 10 aktive Keys pro Account |
| Berechtigungen | Pro Key: read, write, delete — siehe Berechtigungen (Scopes) |
| Deaktivierung | Jederzeit deaktivierbar oder loeschbar |
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.
| Scope | Erlaubt | HTTP-Methoden |
|---|---|---|
| read | Daten abrufen: Listen, Einzelobjekte, Notizen, Dateien (auch Download), Event-Log, API-Root | GET |
| write | Anlegen und Ändern: Leads, Kunden, Notizen, Dateien hochladen, Lead konvertieren, ToDos | POST, PATCH, PUT |
| delete | Unwiderruflich löschen: Leads, Kunden, Lead-Dateien, ToDos | DELETE |
| sign:read | Sign Lab: Dokumente, Vorlagen, Webhooks lesen, signierte PDFs laden (siehe Sign Lab) | GET /v1/sign-* |
| sign:write | Sign Lab: anlegen, senden, erinnern, zurückziehen, Webhooks verwalten | POST, DELETE /v1/sign-* |
- Die Scopes sind unabhängig:
writeschließtreadnicht 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.deletehaben 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:
{
"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. ..."
}
}
read für Reporting, read + write für einen CRM-Sync, delete nur, wenn die Integration wirklich löschen soll.
Base URL & Format
https://api.revenue-lab.de/v1
Endpunkte werden als REST-Pfade angesprochen:
GET /v1/leads— Leads auflistenPOST /v1/leads— Lead erstellenGET /v1/leads/42— Einzelner LeadPATCH /v1/leads/42— Lead aktualisierenPOST /v1/leads/42/notes— Notiz hinzufuegenPOST /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)
{
"success": true,
"data": {
"id": 42,
"name": "Max Mustermann"
}
}
Erfolgreiche Antwort (Liste mit Pagination)
{
"success": true,
"data": [/* Array von Objekten */],
"total": 84,
"limit": 50,
"offset": 0
}
HTTP Status Codes
insufficient_scope)Fehlerbehandlung
Fehlerantworten haben immer "success": false und ein error-Objekt mit maschinenlesbarem Code:
{
"success": false,
"error": {
"code": "missing_field",
"message": "project_id ist erforderlich."
}
}
Fehlercodes
/leads/42/foo/leads//notes)DELETE abgelehntDELETE abgelehntupdated_since ist auf diesem System noch nicht verfügbarRate-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/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)
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.
Antwort-Felder
| Feld | Typ | Beschreibung |
|---|---|---|
| id | integer | Eindeutige Projekt-ID |
| name | string | Projektname |
| type | string | Interner Projekttyp |
| delivery_type | string | Lieferart: termine, leads (Lead-Lieferung an einen Auftraggeber) oder eigen (Inhouse-Akquise). Siehe Lead-Lieferung. |
| lead_value | number|null | Preis je Termin in Euro |
| calendar_link | string|null | Buchungslink des Auftrags |
| agent_id | integer|null | Zugeordneter Agent |
{
"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 https://api.revenue-lab.de/v1/projects \
-H "Authorization: Bearer rl_live_DEIN_API_KEY"
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.
Query-Parameter
| Parameter | Typ | Beschreibung | |
|---|---|---|---|
| project_id | integer | Optional | Nur Leads aus diesem Projekt |
| status | string | Optional | Exakter Status-Match, z.B. Neu oder Terminiert |
| search | string | Optional | Freitextsuche in Name, Firma, Telefon, E-Mail |
| external_ref | string | Optional | Exakter Treffer auf deinen Fremd-Schlüssel — ein Lead per Referenz nachschlagen |
| updated_since | datetime | Optional | Nur Leads, die seit diesem Zeitpunkt angelegt oder geändert wurden (inklusive). Sortierung dann aufsteigend nach updated_at. Siehe Delta-Sync unten. |
| include | string | Optional | custom liefert je Lead die Custom-Field-Werte mit |
| limit | integer | Optional | Ergebnisse pro Seite (1–200, Standard: 50) |
| offset | integer | Optional | Startposition (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_atdes letzten Eintrags als Startpunkt des nächsten Laufs. Die Grenze ist inklusive: entdoppele Treffer über dieid. - 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
{
"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
}
# 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"
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")
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
{
"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"
}
}
{
"success": false,
"error": {
"code": "not_found",
"message": "Lead mit ID 9999 nicht gefunden."
}
}
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).
Request Body
| Feld | Typ | Beschreibung | |
|---|---|---|---|
| project_id | integer | Pflicht | Projekt-ID (via GET /v1/projects abrufbar) |
| name | string | Optional* | Vor- und Nachname |
| company | string | Optional* | Firmenname |
| phone | string | Optional* | Telefonnummer (freies Format, z.B. +49 221 1234567) |
| string | Optional* | E-Mail-Adresse | |
| position | string | Optional | Position / Jobtitel |
| address | string | Optional | Adresse |
| website | string | Optional | Website-URL |
| status | string | Optional | Lead-Status (Standard: Neu). Siehe Status-Werte. |
| notes | string | Optional | Notizen zum Lead |
| followup_at | datetime | Optional | Wiedervorlage-Zeitpunkt (2026-06-10T14:00:00) |
| deal_value | number | Optional | Lead-Wert in Euro (Standard: Projekt-Standardwert) |
| external_ref | string | Optional | Eigener Fremd-Schlüssel (z.B. die ID im Quellsystem), pro Account eindeutig. Aktiviert den Upsert. |
| source | string | Optional | Herkunft des Leads (frei wählbar, z.B. pipedrive) |
| source_note | string | Optional | Zusatzinfo zur Herkunft |
| pipeline_stage | string | Optional | Pipeline-Stage. Ohne Angabe wird sie aus dem status abgeleitet (siehe Status-Werte). |
| lead_type | string | Optional | b2b (Standard) oder b2c |
| salutation | string | Optional | Anrede |
| first_name | string | Optional | Vorname |
| last_name | string | Optional | Nachname |
| legal_form | string | Optional | Rechtsform (z.B. GmbH) |
| termin_at | datetime | Optional | Vereinbarter Termin |
| termin_notes | string | Optional | Notizen 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 (
nulloder"") leert das Feld. Ausgenommen sindname,termin_atundtermin_notes: sie werden durch leere Werte nie gelöscht. - Wird
statusgesendet, aber keinepipeline_stage, wird sie wie üblich aus dem Status abgeleitet. - Das
project_ideines vorhandenen Leads ändert ein Upsert nicht — dafürPATCH /v1/leads/{id}verwenden. - Beim Anlegen gelten weiter die Pflichtangaben (mindestens ein Kontaktfeld).
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" }'
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']}")
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);
{
"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.
Aktualisierbare Felder
Alle Felder sind optional — sende nur was sich ändert. Unbekannte Keys werden silent ignoriert (kein Fehler).
Stammdaten
| Feld | Typ | Beschreibung |
|---|---|---|
| name | string | Voller Name (Anzeige) |
| salutation | string | Anrede (Herr / Frau / Divers) |
| first_name | string | Vorname |
| last_name | string | Nachname |
| company | string | Firma |
| legal_form | string | Rechtsform (GmbH, UG, AG, ...) |
| position | string | Position / Jobtitel |
| lead_type | string | b2b oder b2c |
| phone | string | Telefon |
| string | ||
| website | string | Website |
Adresse
| Feld | Typ | Beschreibung |
|---|---|---|
| address | string | Komplette Adresse als String (Legacy — wenn gesetzt, werden Einzelfelder ignoriert) |
| street | string | Straße + Hausnummer |
| zip | string | Postleitzahl |
| city | string | Ort |
| country | string | Land |
Sales-Pipeline & Deal
| Feld | Typ | Beschreibung |
|---|---|---|
| status | string | Lead-Status. pipeline_stage wird automatisch synchronisiert (siehe Auto-Logik). |
| pipeline_stage | string | Pipeline-Stage explizit setzen. Unterstützt Custom-Stages. Bei is_won-Stages: Auto-Convert greift. |
| lost_reason | string|null | Verlust-Grund (Freitext, nur sinnvoll bei status=Verloren) |
| deal_value | number|null | Deal-Wert in Euro |
| deal_probability | int 0-100|null | Abschlusswahrscheinlichkeit in Prozent. Wird bei "Gewonnen" automatisch auf 100 gesetzt. |
| expected_close_at | date|null | Voraussichtliches Abschluss-Datum (YYYY-MM-DD). Bei "Gewonnen" und vorher leer: heute. |
| source | string | Lead-Quelle (Cold-Call, LinkedIn, Empfehlung, ...) |
| project_id | int|null | Auftrag/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).
| Feld | Typ | Beschreibung |
|---|---|---|
| followup_at | datetime|null | Wiedervorlage-Datum (für Auto-Dialer-Queue) |
| termin_at | datetime|null | Vereinbarter Termin mit dem Lead |
| appointment_set_at | datetime|null | Zeitpunkt der Terminvereinbarung (für Reports) |
Sonstiges
| Feld | Typ | Beschreibung |
|---|---|---|
| notes | string|null | Notiz-Feld (ersetzt den bestehenden Text. Für append: POST /v1/leads/{id}/notes). |
| custom | object | Custom-Field-Werte als {"field_key": "value"}. Siehe Custom Fields. |
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.
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".
# 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" } }'
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']}")
{
"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.
- 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_idwird leer) und der E-Mail-Verlauf im Postfach. - Hat der Lead Verträge, antwortet die API mit
409 lead_has_contractsund 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 -X DELETE "https://api.revenue-lab.de/v1/leads/42" \ -H "Authorization: Bearer rl_live_DEIN_API_KEY"
{
"success": true,
"data": { "deleted": true, "id": 42, "external_ref": "pipedrive:1234" }
}
Datei löschen
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 -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
| Feld | Typ | Beschreibung | |
|---|---|---|---|
| note | string | Pflicht | Text der Notiz (max. 5000 Zeichen). Alias: text. |
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." }'
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": {...}}
{
"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
{
"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).
| Feld | Typ | Beschreibung | |
|---|---|---|---|
| mark_won | boolean | Optional | Setzt den Lead zusaetzlich auf Status Gewonnen. Default: true. |
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 }'
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")
{
"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.
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.
| Feld | Typ | Default | Beschreibung |
|---|---|---|---|
| as_contract | bool | false | Wenn 1/true/yes: zusätzlicher Eintrag in contracts-Tabelle. |
| contract_title | string | Filename ohne Endung | Titel im Verträge-Tab (z. B. "LinkedIn Premium 9M") |
| contract_recipient_name | string | Lead-Name | Empfänger-Anzeige im Verträge-Tab |
| contract_recipient_email | string | Lead-E-Mail | Empfänger-E-Mail im Verträge-Tab |
| contract_status | string | completed | pending | completed | declined | expired. Bei completed: signed_at wird auf jetzt gesetzt. |
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)
file
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"
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": "..."}}
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);
{
"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:
# 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\"}"
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 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
curl "https://dashboard.revenue-lab.de/api?__path=leads/42/files" \ -H "Authorization: Bearer rl_live_DEIN_API_KEY"
{
"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
curl "https://dashboard.revenue-lab.de/api?__path=leads/42/files/17/download" \ -H "Authorization: Bearer rl_live_DEIN_API_KEY" \ -o angebot.pdf
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
- Mit
GET /v1/projectsden Auftrag suchen, dessendelivery_typeden Wertleadshat. - Lead mit
POST /v1/leadsund dieserproject_idanlegen. Mitexternal_refbleibt das idempotent, ein zweiter Aufruf liefert denselben Lead (siehe Upsert). - 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, sowieterminemit 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/leadsliefert sie trotzdem; filtere bei Bedarf mitproject_idoder 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 aufBad 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.createdaus (siehe Webhooks).
Kunden auflisten
Query-Parameter
| Parameter | Typ | Beschreibung | |
|---|---|---|---|
| search | string | Optional | Freitextsuche in Name, Firma und E-Mail |
| updated_since | datetime | Optional | Nur Kunden, die seit diesem Zeitpunkt angelegt oder geändert wurden (wie beim Lead-Delta-Sync) |
| include | string | Optional | custom liefert je Kunde die Custom-Field-Werte mit |
| limit | integer | Optional | Max. Ergebnisse (1–200, Standard: 50) |
| offset | integer | Optional | Startposition (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.
{
"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 "https://api.revenue-lab.de/v1/customers?search=Mustermann" \
-H "Authorization: Bearer rl_live_DEIN_API_KEY"
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
curl "https://api.revenue-lab.de/v1/customers/12" \
-H "Authorization: Bearer rl_live_DEIN_API_KEY"
{
"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.
Hat der Kunde Rechnungen oder Abos, antwortet die API mit 409 customer_has_billing und löscht nichts — bitte im CRM prüfen.
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.
Query-Parameter
| Parameter | Typ | Beschreibung |
|---|---|---|
| column_id | int | Nur Karten dieser Spalte (Spalten-IDs via /board sichtbar). |
| assigned_user_id | int | Nur Karten, die diesem User zugewiesen sind. |
| completed | 0|1 | 1 = nur Karten in der letzten Spalte (Erledigt). 0 = alle anderen. |
| limit | int | Max 200 (Default 50). |
| offset | int | Pagination-Offset. |
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
{
"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.
Body (JSON)
| Feld | Typ | Beschreibung |
|---|---|---|
| title * | string | Karten-Titel. Pflicht. |
| description | string | Beschreibung / Notiz. |
| column_id | int | Optional — ohne Angabe landet die Karte in der ersten Spalte. |
| color_label | string | Hex-Farbe der Karte (z.B. #4a8fff). Default Blau. |
| due_date | string | Faelligkeit (YYYY-MM-DD). |
| assigned_user_id | int | User-ID, dem die Karte zugewiesen wird. |
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 }'
{
"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.
Alle Felder optional. Felder die nicht im Body stehen, bleiben unveraendert.
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 }'
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
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
| Scope | Erlaubt |
|---|---|
| sign:read | GET auf /v1/sign-documents, /v1/sign-templates, /v1/sign-webhooks inkl. Download und Prüfprotokoll |
| sign:write | POST/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.
| Status | Bedeutung | Filter |
|---|---|---|
| document.draft | Entwurf, kann bearbeitet und gesendet werden | 0 |
| document.waiting_approval | von einem KI-Agenten angelegt, wartet auf Admin-Freigabe | 6 |
| document.sent | versendet, noch niemand hat geöffnet | 1 |
| document.viewed | versendet, mindestens ein Empfänger hat geöffnet | 5 |
| document.completed | alle haben unterschrieben, PDF versiegelt | 2 |
| document.declined | ein Empfänger hat abgelehnt | 12 |
| document.expired | Frist abgelaufen | expired |
| document.voided | vom Absender zurückgezogen | 11 |
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
{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) | Typ | Beschreibung |
|---|---|---|
| name | string | Titel des Dokuments |
| template_uuid | string | Vorlage (ID aus /v1/sign/templates); sonst file |
| file | string | PDF als Base64 (max. 20 MB). Ein Abruf per url wird aus Sicherheitsgründen nicht unterstützt. |
| recipients | array | [{"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. |
| tokens | array | [{"name":"company","value":"…"}] füllt bzw. überschreibt Platzhalter der Vorlage |
| fields | object | array | Vorlage: {"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://…)). |
| metadata | object | {"lead_id":128} oder {"kunde_id":7}: Platzhalter aus dem Kontakt, Ablage des signierten PDFs |
| message, expiration_days, reminder_days | mixed | Nachricht 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 -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
/details liefert roles (name, signing_order, source: contact | sender | fixed), tokens (verfügbare Platzhalter) und fields mit assigned_to.role.
Webhook-Subscriptions und -Events
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
/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 -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
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).
| Feld | Typ | Beschreibung |
|---|---|---|
| template_id | int | Vorlage (Variante 1) |
| lead_id / kunde_id | int | Kontakt: füllt Platzhalter, das signierte PDF wird dort abgelegt |
| signers | array | [{"name","email","role"}] in Reihenfolge; bei Vorlagen überschreibt es Name/E-Mail je Rolle |
| file_base64, filename, title | string | PDF (max. 20 MB, ohne Passwortschutz und Skripte), Variante 2 |
| fields | array | {"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_order | string | sequential (Standard, serverseitig erzwungen) oder parallel |
| message, expires_days, reminder_days | mixed | Nachricht in der Einladung, Gültigkeit (1–365 Tage), Erinnerung alle n Tage (0 = aus, höchstens 3) |
| send | bool | true = sofort versenden, sonst bleibt es ein Entwurf. Fehlt etwas, steht es in problems. |
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
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)
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.
$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
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.
readfürlist_claims,get_claim,get_order_figures;sendfürpropose_claim_decisionundcancel_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
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
| Argument | Typ | Beschreibung |
|---|---|---|
| status | string | offen (Standard), akzeptiert, abgelehnt oder alle |
| project_id | integer | Nur dieser Auftrag |
| limit | integer | Standard 50, höchstens 200 |
get_claim mit lead_id liefert denselben Eintrag für einen Lead. Ein Eintrag:
{
"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:leadodertermin. Bei Terminen istdelivered_atdie Buchung undfeedbackenthältoutcome,happened_at,noshow_at,termin_at,notes.counts_for_billingistfalse, wenn der Lead nicht zählt (Bad Data, vor Lieferbeginn); der Grund steht innot_counted_reason.
propose_claim_decision — Entscheidung vorschlagen
| Argument | Typ | Beschreibung |
|---|---|---|
| lead_id | integer | Lead mit offener Reklamation (sonst Fehler) |
| decision | string | akzeptiert (anerkennen) oder abgelehnt |
| note | string | Begrü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
| Argument | Typ | Beschreibung |
|---|---|---|
| project_id | integer | Nur dieser Auftrag; ohne Angabe alle Lead- und Termin-Aufträge |
| from | string | Erster Tag, z. B. 2026-10-12 |
| to | string | Letzter Tag (einschließlich), z. B. 2026-10-18 |
{
"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.
Trigger-Ereignisse
| Event | Wann | Action 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 |
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.*.
{
"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
| Feld | Typ | Beschreibung |
|---|---|---|
| subscription_id | string | ID der Automation. Prefix: whsub_. |
| tenant_id | integer | Dein Account / Mandant. |
| event.id | string | Eindeutige Event-ID, Prefix ev_. Nutzbar zur Deduplizierung / Idempotenz. |
| event.date_created | string | ISO-8601 Timestamp mit Zeitzone. |
| event.object_type | string | Aktuell immer "lead". |
| event.object_id | string | Sprechende ID, z.B. "lead_42". Numerisch auch als event.lead_id. |
| event.action | string | "created" oder "updated". |
| event.trigger | string | Das ausloesende Ereignis: lead.created, pipeline.update, appointment.set. |
| event.changed_fields | string[] | Liste der geaenderten Feldnamen. Bei created: alle befuellten Felder. |
| event.data | object | Vollstaendiger aktueller Lead-Zustand. Custom Fields unter data.custom.<key>. |
| event.previous_data | object | Alte Werte der geaenderten Felder (Diff). Bei created leer: {}. |
| event.actor | object | Ausloesender Nutzer mit id und name. |
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.
Query-Parameter
| Parameter | Typ | Beschreibung |
|---|---|---|
| category | string | api (eingehende Calls) oder webhook (ausgehende Zustellungen). Leer = beides. |
| limit | integer | Max. Ergebnisse (1–200, Standard: 50) |
| offset | integer | Startposition (Standard: 0) |
{
"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:
| Phase | Wer triggert | API-Call |
|---|---|---|
| Erstellung | Externe Quelle (Website-Formular, Werbe-Lead, Import) | POST /v1/leads |
| Kontakt & Pflege | Agent im Dialer + externe Tools (E-Mail-Antwort, SMS) | PATCH /v1/leads/{id} + POST /v1/leads/{id}/notes |
| Angebot/Vertrag | Vertragstool (PandaDoc, Documenso) via n8n | POST /v1/leads/{id}/files mit as_contract=1 |
| Abschluss | Vertragstool / Sales-Agent | PATCH /v1/leads/{id} mit status=Gewonnen → Auto-Logik feuert |
| Kundenpflege | CRM-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.
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}mitstatus=Gewonnen— Auto-Convert prüft ob schon ein Kunde da ist und legt nur fehlende an.POST /v1/leads/{id}/filesmitas_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
| # | Aktion | Bedingung |
|---|---|---|
| 1 | Kunde 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). |
| 2 | Kunden-Aktivität "system" angelegt — sichtbar im Kunden-Drawer. | Immer, wenn (1) feuert. |
| 3 | status wird auf Gewonnen gesetzt (auch bei Custom-is_won-Stages). | Nur wenn Client nicht explizit anderen Status mitgab. |
| 4 | deal_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. |
| 5 | expected_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. |
| 6 | Audit-Eintrag wird angelegt (vertrag_unterzeichnet bei Datei-Upload als Vertrag, sonst pipeline_stage-Änderung). | Immer. |
| 7 | Outbound-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:
// 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"
| Weg | Call | Wann nutzen |
|---|---|---|
| PATCH status | PATCH /v1/leads/{id}{ "status": "Gewonnen" } | Einfachster Weg. Wenn dein Workflow eh PATCH-Felder updatet (Deal-Wert, Notiz, Custom-Felder). |
| PATCH pipeline_stage | PATCH /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_contract | POST /v1/leads/{id}/filesmultipart 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. |
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 }:
{
"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 /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:
{
"success": true,
"data": { /* ... inkl. custom-Objekt mit neuen Werten ... */ },
"custom_saved": ["angebotspaket", "kampagnenstart", "preisnetto"]
}
"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 — imcustom-Objekt siehst du alle Keys - Oder: einen Webhook für
lead.createdoderpipeline.updateanlegen — im Payload stehtevent.data.custommit allen Keys
Lead-Status-Werte
Erlaubte Werte fuer das Feld status bei POST /v1/leads und PATCH /v1/leads/{id}:
| Status | Beschreibung | Pipeline-Stage | In Dialer-Queue? |
|---|---|---|---|
| Neu | Neuer Lead, noch nicht kontaktiert (Standard) | In Auftrag | Ja |
| Wiedervorlage | Rueckruf geplant — erscheint zum Wiedervorlage-Zeitpunkt | In Auftrag | Ja |
| Nicht erreicht | Anrufversuch ohne Erreichung | In Auftrag | Ja |
| Entscheider nicht erreicht | Nur Sekretariat/Assistent erreicht | In Auftrag | Ja |
| Terminiert | Termin wurde vereinbart | In Auftrag | Nein |
| Kein Interesse | Lead hat abgelehnt | In Auftrag | Nein |
| Bad Data | Fehlerhafte Kontaktdaten | Bad Data | Nein |
| Info Mail angefordert | Infomaterial angefordert | In Auftrag | Nein |
| Info Mail versendet | Infomaterial wurde versendet | In Auftrag | Nein |
| Gewonnen | Deal abgeschlossen — löst Auto-Convert-Kaskade aus | Gewonnen | Nein |
| Verloren | Deal verloren (Grund optional via lost_reason) | Verloren | Nein |
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:
| Feld | Typ | Beschreibung |
|---|---|---|
| id | integer | Eindeutige Lead-ID |
| project_id | integer | Zugehoeriges Projekt |
| project_name | string | Projektname (read-only, JOIN) |
| name | string|null | Vor- und Nachname |
| salutation / first_name / last_name | string|null | Aufgespaltene Namen |
| company | string|null | Firmenname |
| legal_form | string|null | Rechtsform |
| position | string|null | Position / Jobtitel |
| lead_type | string | b2b oder b2c |
| phone | string|null | Telefonnummer |
| string|null | E-Mail-Adresse | |
| website | string|null | Website-URL |
| address | string|null | Adresse als ganzer String (Legacy) |
| street / zip / city / country | string|null | Aufgespaltene Adressfelder |
| status | string | Lead-Status (siehe Status-Werte) |
| pipeline_stage | string | Aktuelle Pipeline-Stage (Standard oder Custom) |
| lost_reason | string|null | Verlust-Grund (nur bei status=Verloren) |
| source | string|null | Lead-Quelle |
| deal_value | number|null | Deal-Wert in Euro |
| deal_probability | int|null | Abschlusswahrscheinlichkeit (0–100). Bei "Gewonnen" auto auf 100. |
| expected_close_at | date|null | Voraussichtl. Abschluss-Datum (YYYY-MM-DD) |
| termin_at | datetime|null | Vereinbarter Termin mit dem Lead |
| appointment_set_at | datetime|null | Zeitpunkt der Terminvereinbarung |
| followup_at | datetime|null | Wiedervorlage-Zeitpunkt |
| notes | string|null | Notizen-Text (Single-Field — für Verlauf: /v1/leads/{id}/notes) |
| custom | object | Custom-Field-Werte (siehe Custom Fields) |
| external_ref | string|null | Dein Fremd-Schlüssel (siehe Upsert); über GET /v1/leads?external_ref=… abrufbar |
| source_note | string|null | Zusatzinfo zur Herkunft |
| termin_notes | string|null | Notizen zum vereinbarten Termin |
| created_at | datetime | Erstellungszeitpunkt |
| updated_at | datetime | Letzter 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.
Pagination
Alle Listen-Endpunkte nutzen Offset-Pagination ueber limit und offset.
| Parameter | Default | Max | Beschreibung |
|---|---|---|---|
| limit | 50 | 200 | Ergebnisse pro Seite |
| offset | 0 | — | Ueberspringt die ersten N Ergebnisse |
Alle Seiten laden
import requests API_KEY = "rl_live_DEIN_API_KEY" BASE = "https://api.revenue-lab.de/v1" headers = {"Authorization": f"Bearer {API_KEY}"} all_leads = [] offset = 0 limit = 200 # Maximum while True: res = requests.get(f"{BASE}/leads", headers=headers, params={"limit": limit, "offset": offset}) page = res.json() all_leads.extend(page["data"]) if len(all_leads) >= page["total"]: break offset += limit print(f"{len(all_leads)} Leads geladen")
async function getAllLeads() { const API_KEY = 'rl_live_DEIN_API_KEY'; const BASE = 'https://api.revenue-lab.de/v1'; const all = []; let offset = 0; const limit = 200; while (true) { const res = await fetch(`${BASE}/leads?limit=${limit}&offset=${offset}`, { headers: { 'Authorization': `Bearer ${API_KEY}` } }); const page = await res.json(); all.push(...page.data); if (all.length >= page.total) break; offset += limit; } return all; }
Zapier, Make & n8n Integration
n8n — HTTP Request Node
- Node: HTTP Request
- Method:
POST - URL:
https://api.revenue-lab.de/v1/leads - Authentication: Header Auth
- Header Name:
Authorization— Value:Bearer rl_live_DEIN_API_KEY - Body Type:
JSON - Body: Felder mappen (project_id, name, phone, email, ...)
n8n — Webhook empfangen
- Node: Webhook (Trigger)
- HTTP Method:
POST - Webhook-URL kopieren und in Settings → Automationen als Ziel-URL eintragen
- Daten liegen unter
body.event.data.*undbody.event.changed_fields
Make (Integromat) — HTTP-Modul
- Modul: HTTP → Make a request
- URL:
https://api.revenue-lab.de/v1/leads - Method:
POST - Headers:
Authorization→Bearer rl_live_DEIN_API_KEY - Body type:
Raw→application/json
Zapier — Webhooks by Zapier
- Trigger: Beliebig (Typeform, Jotform, Google Forms, ...)
- Action: Webhooks by Zapier → POST
- URL:
https://api.revenue-lab.de/v1/leads - Payload Type:
json - Headers:
Authorization | Bearer rl_live_DEIN_API_KEY
{
"project_id": 1,
"name": "{{Name}}",
"email": "{{Email}}",
"phone": "{{Telefon}}",
"company": "{{Firma}}",
"notes": "Quelle: {{Formularname}}"
}
GET /v1/projects aufrufen, die gewuenschte ID notieren und im Workflow fest hinterlegen.