Liftaro Partner API
REST-API für Aufzugsunternehmen zur direkten Software-Integration mit der Liftaro Vergleichsplattform. Empfangt Anfragen, gebt Angebote zurück — alles aus eurem eigenen CRM / Quote-System.
Workflow
- Anfrage: Liftaro-Sachbearbeiter löst Partner-Anfrage aus → ihr seht sie via Polling oder Webhook (
anfrage.created) - Angebot abgeben: Ihr pickt aus eurer Offer-DB ein passendes Angebot + submittet via
POST /api/v2/checks/{id}/offermit Eckdaten + optional PDF.
⚠ Vertragsentwurf bitte mit „Liftaro GmbH (vermittelnd)" als Platzhalter-Adressat ausstellen. - HV-Entscheidung: Liftaro zeigt dem HV alle Varianten. HV wählt eine aus.
- Auftrag erteilt: Bei eurer Variante → Webhook
anfrage.auftrag_erteiltmit echten HV-Daten (Name, Anschrift, Email, Telefon, Ansprechpartner). - Finaler Vertrag (über Liftaro): Ihr erstellt den finalen Wartungsvertrag mit der echten HV als Adressat und schickt ihn zurück an Liftaro via
POST /api/v2/checks/{id}/final-vertrag(mit base64-PDF) ODER per Email ancheck@liftaro.de. Bitte nicht direkt mit der HV kommunizieren. - Liftaro leitet weiter + Unterschrift: Liftaro schickt den Vertrag zur Unterschrift an die HV. Nach Unterschrift erhaltet ihr den signierten Vertrag zurück.
- Vertrag entsteht zwischen euch und der HV. Liftaro tritt heraus, erhält die im Rahmenvertrag vereinbarte Vermittlungsprovision. Ab jetzt direkter Kontakt mit HV für Wartungsausführung.
Authentifizierung
Jeder Request muss den Header X-Partner-Key mitsenden:
X-Partner-Key: <euer-api-key>
Den API-Key bekommt ihr von Liftaro (Email an check@liftaro.de). Er ist dauerhaft, kann aber bei Verdacht auf Kompromittierung rotiert werden.
Basis-URL
https://giltglobalinvest--4c42aaf257b611f19fa0ee650bb23af1.web.val.run
Health-Check ohne Auth: GET /api/health → {"ok":true, ...}
Endpoints
Verifiziert den API-Key und gibt die Partner-Metadaten zurück. Erster Aufruf zur Validierung empfohlen.
# Response 200
{
"ok": true,
"partner_id": "ahw",
"name": "Aufzugshandwerk GmbH",
"email_kontakt": "vertrieb@ahw.de",
"active": true,
"api_version": "v2",
"webhook_url": null,
"webhook_configured": false
}
🚀 Try it out
Listet alle Anfragen für euren Partner-Account. Standard-Filter: offen (= gesendet + geoeffnet). Sortiert nach Versand-Datum absteigend.
Query-Parameter
| Name | Werte | Default |
|---|---|---|
status | offen, alle, gesendet, geoeffnet, angeboten, abgelehnt, timeout | offen |
# Response 200
{
"ok": true,
"count": 2,
"anfragen": [
{
"anfrage_id": "PA-2026-6d0e",
"status": "geoeffnet",
"objekt_ort": "Stuttgart",
"anzahl_aufzuege": "3",
"deadline_at": "2026-05-31T20:00:00.000Z"
}
]
}
🚀 Try it out
Volle Details. Beim ersten Aufruf wird Status gesendet → geoeffnet und opened_at gesetzt. Sicherheit: Anfrage muss zu eurem Partner gehören (sonst 403).
# Response 200
{
"ok": true,
"anfrage": {
"anfrage_id": "PA-2026-6d0e",
"status": "geoeffnet",
"sent_at": "2026-05-24T20:00:00.000Z",
"deadline_at": "2026-05-31T20:00:00.000Z",
"objekt_plz": "70173",
"objekt_ort": "Stuttgart",
"objekt_strasse": "Mustergasse 12",
"anzahl_aufzuege": "2",
"anlagenart": "Personenaufzug",
"leistungsumfang_text": "Standard-Wartung 2x/J inkl. Notruf",
"include_vorliegendes_pdf": false,
"vorliegendes_pdf_url": null,
"response_preis_netto": null,
"response_kommentar": null
}
}
🚀 Try it out
Submittet euer Angebot. Status wechselt auf angeboten. PDF-Anhang dringend empfohlen (max 8 MB) — ohne PDF kann die HV nicht entscheiden und Liftaro muss manuell nachfassen. Doppel-Submit wird mit 409 abgelehnt.
pdf_base64 mitschicken. Wir leiten das PDF 1:1 mit der Vergleichsanalyse an die Hausverwaltung weiter. Angebote ohne PDF werden von uns nicht direkt an die HV gegeben — wir fragen euch zuerst nach dem Angebotsdokument. Aktuell technisch noch optional (für Übergangszeit), wird in zukünftigen API-Versionen Pflicht.
Request-Body
| Feld | Typ | Pflicht? | Beispiel |
|---|---|---|---|
response_offer_id | string | nein | "ANG-2026-9911" |
response_preis_netto | string (€/Jahr) | empfohlen | "1980.00" |
response_laufzeit_jahre | string | empfohlen | "3" |
response_kuendigungsfrist_monate | string | nein | "3" |
response_wartungen_pro_jahr | string | nein | "2" |
response_vollwartung | boolean | nein | true |
response_notruf | boolean | nein | true |
response_tuev_begl | boolean | nein | true |
response_tuev_pruef | boolean | nein | false |
response_entstoerung | boolean | nein | true |
response_kommentar | string (max 4000) | nein | "Anfahrt inkl. Region Stuttgart" |
pdf_base64 | string (Base64) | ★ dringend empfohlen | "JVBERi0xLjQK…" |
pdf_filename | string | mit pdf_base64 | "angebot.pdf" |
# Response 200
{ "ok": true, "status": "angeboten", "channel": "api" }
🚀 Try it out
Markiert die Anfrage als abgelehnt. Optional Grund.
# Request
{ "reason": "Anlagen-Typ außerhalb unseres Service-Gebiets" }
# Response 200
{ "ok": true, "status": "abgelehnt", "channel": "api" }
🚀 Try it out
Webhooks
Statt aktiv zu pollen, könnt ihr eine Webhook-URL hinterlegen. Sobald eine neue Anfrage für euren Partner-Account erstellt wird, sendet Liftaro automatisch einen POST an eure URL — mit allen Eckdaten + signiertem Body.
Einrichtung
Liftaro hinterlegt eure Webhook-URL im Partner-Pool. Sag uns:
- Eure HTTPS-Empfänger-URL (z.B.
https://your-crm.example.com/integrations/liftaro) - Optional: Dass wir den Signature-Header mit einem bestimmten Secret signieren (Default: euer API-Key wird als HMAC-Secret verwendet)
Webhook-Payload
POST mit JSON-Body. Header:
Content-Type: application/jsonX-Liftaro-Signature: sha256=<hex>— HMAC-SHA256 des Body, signiert mit euremapi_keyX-Liftaro-Event: anfrage.created— Event-NameUser-Agent: liftaroPartnerApi/1.0 (+https://check.liftaro.de)
# Body bei "anfrage.created" (= neue Anfrage von Liftaro)
{
"event": "anfrage.created",
"timestamp": "2026-05-25T12:34:56.789Z",
"anfrage_id": "PA-2026-6d0e",
"partner_id": "ahw",
"sent_at": "2026-05-25T12:34:56.789Z",
"deadline_at": "2026-05-30T12:34:56.789Z",
"objekt_plz": "70173",
"objekt_ort": "Stuttgart",
"objekt_strasse": "Mustergasse 12",
"anzahl_aufzuege": "2",
"anlagenart": "Personenaufzug",
"leistungsumfang_text": "Standard-Wartung 2x/J inkl. Notruf",
"landing_url": "https://check.liftaro.de/partner/anfrage.html?t=…"
}
# Body bei "anfrage.auftrag_erteilt" (= HV hat zugestimmt, hier die HV-Daten)
# → Jetzt finalen Vertrag mit final_hv_* Daten erstellen und an HV schicken
{
"event": "anfrage.auftrag_erteilt",
"timestamp": "2026-05-26T09:00:00.000Z",
"anfrage_id": "PA-2026-6d0e",
"partner_id": "ahw",
"auftrag_erteilt_at": "2026-05-26T09:00:00.000Z",
"final_hv_name": "Hausverwaltung Müller GmbH",
"final_hv_anschrift": "Bahnhofstr. 12, 89075 Ulm",
"final_hv_email": "info@hv-mueller.de",
"final_hv_telefon": "+49 731 555-123",
"final_hv_ansprechpartner": "Frau Müller",
"objekt_plz": "70173",
"objekt_ort": "Stuttgart",
"response_preis_netto": "1980.00",
"next_step": "Bitte erstellen Sie den finalen Vertrag mit '…' als Adressat und schicken ihn ZURÜCK AN LIFTARO (API .../final-vertrag oder Email check@liftaro.de). NICHT direkt an HV."
}
Signature-Verifikation
So prüft ihr in eurem Endpoint, ob der Request echt von Liftaro kommt (verhindert gefälschte Webhooks):
import crypto from 'crypto';
app.post('/integrations/liftaro', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.header('X-Liftaro-Signature') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.LIFTARO_API_KEY)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(req.body);
// payload.event, payload.anfrage_id, etc.
res.json({ ok: true });
});
import hmac, hashlib
from flask import request
@app.route('/integrations/liftaro', methods=['POST'])
def liftaro_webhook():
body = request.get_data()
sig = request.headers.get('X-Liftaro-Signature', '')
expected = 'sha256=' + hmac.new(
os.environ['LIFTARO_API_KEY'].encode(),
body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig, expected):
return 'Invalid signature', 401
payload = request.get_json()
# payload['event'], payload['anfrage_id'], ...
return {'ok': True}
<?php
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_LIFTARO_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $body, getenv('LIFTARO_API_KEY'));
if (!hash_equals($signature, $expected)) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($body, true);
// $payload['event'], $payload['anfrage_id'], ...
echo json_encode(['ok' => true]);
Anforderungen an euren Endpoint
- HTTPS only (kein HTTP)
- Antwortzeit < 8s — sonst Timeout, Webhook gilt als gescheitert
- Idempotent: ihr könntet denselben Webhook mehrfach empfangen (Re-Try-Logik geplant). Speichert
anfrage_idals Idempotency-Key. - 200/201/202 = ok, alles andere = fail (Liftaro loggt, kein automatisches Retry derzeit)
Sendet einen webhook.test-Payload an die im Partner-Pool hinterlegte webhook_url — zur Verifikation bei Einrichtung. Kein Body nötig.
# Response 200
{ "ok": true, "sent_to": "https://your-crm.example.com/integrations/liftaro", "event": "webhook.test" }
🚀 Try it out
Voraussetzung: Liftaro hat im Partner-Pool für euren Account die webhook_url gesetzt.
Datenmodell
Eine vollständige Anfrage (Object anfrage):
| Feld | Typ | Beschreibung |
|---|---|---|
anfrage_id | string | Eindeutige ID, Format PA-YYYY-XXXX |
status | enum | gesendet, geoeffnet, angeboten, abgelehnt, timeout |
sent_at | ISO-8601 | Erstellt am |
opened_at | ISO-8601 / null | Erstmals abgerufen am |
responded_at | ISO-8601 / null | Beantwortet/abgelehnt am |
deadline_at | ISO-8601 | Antwortfrist |
objekt_plz, _ort, _strasse | string | Objektadresse (Straße evtl. anonymisiert) |
anzahl_aufzuege | string | Anzahl Aufzüge (als String, da auch "2-3" möglich) |
anlagenart | string | z.B. "Personenaufzug, 8 Personen" |
leistungsumfang_text | multiline string | Freitext-Soll-Beschreibung |
vorliegendes_pdf_url | signierte URL / null | Bestands-Angebot zum Vergleich (falls Liftaro mitsendet) — Link läuft nach Stunden ab |
response_* | verschieden | Eure Antwort, falls bereits abgegeben (read-only nach Submit) |
Status-Codes
| Code | Bedeutung | Wann |
|---|---|---|
| 200 | Success | Alles ok |
| 400 | Bad Request | JSON nicht parsbar oder Pflichtfeld fehlt |
| 401 | Unauthorized | X-Partner-Key fehlt oder ungültig |
| 403 | Forbidden | Anfrage gehört nicht zu eurem Partner-Account |
| 404 | Not Found | Anfrage-ID existiert nicht |
| 409 | Conflict | Status bereits final (angeboten oder abgelehnt) |
| 410 | Gone | Anfrage abgelaufen (timeout) |
| 500 | Server Error | Backend-Fehler — Retry mit Exponential-Backoff |
Alle Fehler haben die Form { "error": "Beschreibung" }.
Code-Beispiele
# 1) Self-Check
curl -H "X-Partner-Key: $KEY" \
https://giltglobalinvest--4c42aaf257b611f19fa0ee650bb23af1.web.val.run/api/v2/me
# 2) Offene Anfragen pollen (alle 5 Min)
curl -H "X-Partner-Key: $KEY" \
"https://.../api/v2/checks?status=offen"
# 3) Details holen
curl -H "X-Partner-Key: $KEY" \
https://.../api/v2/checks/PA-2026-1234
# 4) Angebot abgeben
curl -X POST -H "X-Partner-Key: $KEY" -H "Content-Type: application/json" \
https://.../api/v2/checks/PA-2026-1234/offer \
-d '{
"response_offer_id":"ANG-2026-9911",
"response_preis_netto":"1980.00",
"response_laufzeit_jahre":"3",
"response_kuendigungsfrist_monate":"3",
"response_wartungen_pro_jahr":"2",
"response_vollwartung":true,
"response_notruf":true,
"response_tuev_begl":true,
"response_kommentar":"Standardvertrag, Anfahrt inkl."
}'
const API = 'https://giltglobalinvest--4c42aaf257b611f19fa0ee650bb23af1.web.val.run';
const KEY = process.env.LIFTARO_PARTNER_KEY;
const HEAD = { 'X-Partner-Key': KEY, 'Content-Type': 'application/json' };
// Offene Anfragen abrufen
const resp = await fetch(`${API}/api/v2/checks?status=offen`, { headers: HEAD });
const { anfragen } = await resp.json();
for (const a of anfragen) {
console.log(a.anfrage_id, a.objekt_ort, a.deadline_at);
}
// Angebot abgeben
await fetch(`${API}/api/v2/checks/PA-2026-1234/offer`, {
method: 'POST',
headers: HEAD,
body: JSON.stringify({
response_offer_id: 'ANG-2026-9911',
response_preis_netto: '1980.00',
response_laufzeit_jahre: '3',
response_wartungen_pro_jahr: '2',
response_vollwartung: true,
response_notruf: true,
}),
});
import os, requests
API = 'https://giltglobalinvest--4c42aaf257b611f19fa0ee650bb23af1.web.val.run'
KEY = os.environ['LIFTARO_PARTNER_KEY']
HEAD = {'X-Partner-Key': KEY}
# Offene Anfragen pollen
r = requests.get(f'{API}/api/v2/checks?status=offen', headers=HEAD)
for a in r.json()['anfragen']:
print(a['anfrage_id'], a['objekt_ort'], a['deadline_at'])
# Angebot abgeben
requests.post(
f'{API}/api/v2/checks/PA-2026-1234/offer',
headers={**HEAD, 'Content-Type': 'application/json'},
json={
'response_offer_id': 'ANG-2026-9911',
'response_preis_netto': '1980.00',
'response_laufzeit_jahre': '3',
'response_wartungen_pro_jahr': '2',
'response_vollwartung': True,
'response_notruf': True,
},
)
<?php
$API = 'https://giltglobalinvest--4c42aaf257b611f19fa0ee650bb23af1.web.val.run';
$KEY = getenv('LIFTARO_PARTNER_KEY');
// Offene Anfragen pollen
$ch = curl_init("$API/api/v2/checks?status=offen");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-Partner-Key: $KEY"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
foreach ($response['anfragen'] as $a) {
echo $a['anfrage_id'] . ' ' . $a['objekt_ort'] . PHP_EOL;
}
// Angebot abgeben
$ch = curl_init("$API/api/v2/checks/PA-2026-1234/offer");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-Partner-Key: $KEY", "Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'response_preis_netto' => '1980.00',
'response_laufzeit_jahre' => '3',
'response_vollwartung' => true,
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
using System.Net.Http;
using System.Net.Http.Json;
var http = new HttpClient { BaseAddress = new Uri("https://giltglobalinvest--4c42aaf257b611f19fa0ee650bb23af1.web.val.run") };
http.DefaultRequestHeaders.Add("X-Partner-Key", Environment.GetEnvironmentVariable("LIFTARO_PARTNER_KEY"));
// Offene Anfragen pollen
var resp = await http.GetFromJsonAsync<dynamic>("/api/v2/checks?status=offen");
foreach (var a in resp.anfragen) {
Console.WriteLine($"{a.anfrage_id} {a.objekt_ort}");
}
// Angebot abgeben
await http.PostAsJsonAsync("/api/v2/checks/PA-2026-1234/offer", new {
response_preis_netto = "1980.00",
response_laufzeit_jahre = "3",
response_vollwartung = true,
response_notruf = true,
});
Integrations-Pattern
Empfohlen: Polling alle 5 Minuten
Eure Software ruft GET /api/v2/checks?status=offen alle 5 Minuten auf. Neue Anfragen werden in eurem internen Menü (z.B. „Liftaro Checks") sichtbar.
# Pseudo-Code
every 5 minutes:
open_anfragen = GET /api/v2/checks?status=offen
for a in open_anfragen:
if a.anfrage_id not in known:
show_notification("Neue Liftaro-Anfrage", a)
known.add(a.anfrage_id)
Optional: Webhooks (geplant)
Push-Notifications statt Polling sind in Vorbereitung. Sobald verfügbar, könnt ihr eine Callback-URL bei Liftaro registrieren.
Test-Modus
Für Sandbox-Tests fragt ihr Liftaro nach einem zweiten Test-API-Key. Damit könnt ihr alle Endpoints durchspielen ohne echte Anfragen.
FAQ
Wie ist die SLA?
Antwortzeit innerhalb der deadline_at (Standard 5 Werktage). Bei Nicht-Antwort wechselt die Anfrage auf timeout und ist nicht mehr beantwortbar.
Können wir das vorliegende Bestands-Angebot sehen?
Nur wenn Liftaro es explizit mitsendet (include_vorliegendes_pdf=true). Default ist nein, weil Marktbenchmarking fair bleiben soll.
Bekommen wir den Endkunden-Kontakt?
Erst nach Annahme eures Angebots durch den Endkunden. Bis dahin: nur anonymisierte Objektdaten.
Wer stellt den Vertrag aus?
Liftaro. Ihr seid Subunternehmer. Die Abrechnung läuft zwischen Liftaro und euch separat.
Was passiert bei Doppel-Submit?
Zweiter POST /offer mit demselben Anfrage-ID liefert 409 Conflict. Ihr könnt mit GET /api/v2/checks/{id} den aktuellen Stand abrufen.
Können wir Anfragen löschen?
Nein. Ihr könnt aber POST /decline aufrufen, dann wird sie als abgelehnt markiert. Liftaro fragt eventuell einen anderen Partner.
Rate-Limits?
Aktuell keine harten Limits. Bei missbräuchlicher Last (>1 Req/sec über längere Zeit) kann der Key gesperrt werden. Reasonable Polling (alle 5 Min) ist absolut ok.