liftaro

Partner API — Doku

REST · Kanal 2 · v2
OpenAPI Spec

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.

Was ist Liftaro? Liftaro ist eine unabhängige Vergleichsplattform. Hausverwaltungen reichen ihren Bestands-Wartungsvertrag ein, Liftaro fragt mehrere Aufzugsunternehmen nach Alternativangeboten. Der finale Vertrag wird auf Liftaro-Briefpapier ausgestellt — ihr agiert als Subunternehmer, eure Konditionen bleiben anonym gegenüber dem Endkunden.

Workflow

  1. Anfrage: Liftaro-Sachbearbeiter löst Partner-Anfrage aus → ihr seht sie via Polling oder Webhook (anfrage.created)
  2. Angebot abgeben: Ihr pickt aus eurer Offer-DB ein passendes Angebot + submittet via POST /api/v2/checks/{id}/offer mit Eckdaten + optional PDF.
    Vertragsentwurf bitte mit „Liftaro GmbH (vermittelnd)" als Platzhalter-Adressat ausstellen.
  3. HV-Entscheidung: Liftaro zeigt dem HV alle Varianten. HV wählt eine aus.
  4. Auftrag erteilt: Bei eurer Variante → Webhook anfrage.auftrag_erteilt mit echten HV-Daten (Name, Anschrift, Email, Telefon, Ansprechpartner).
  5. 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 an check@liftaro.de. Bitte nicht direkt mit der HV kommunizieren.
  6. Liftaro leitet weiter + Unterschrift: Liftaro schickt den Vertrag zur Unterschrift an die HV. Nach Unterschrift erhaltet ihr den signierten Vertrag zurück.
  7. 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.
🔑 Try-it-out API-Key kein Key gesetzt

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.

Sicherheit: Speichert den Key serverseitig (env-Variable / Secret-Manager). Niemals im Frontend / Mobile-Client einbetten.

Basis-URL

https://giltglobalinvest--4c42aaf257b611f19fa0ee650bb23af1.web.val.run

Health-Check ohne Auth: GET /api/health{"ok":true, ...}

Endpoints

GET /api/v2/me

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
GET /api/v2/checks?status=offen

Listet alle Anfragen für euren Partner-Account. Standard-Filter: offen (= gesendet + geoeffnet). Sortiert nach Versand-Datum absteigend.

Query-Parameter

NameWerteDefault
statusoffen, alle, gesendet, geoeffnet, angeboten, abgelehnt, timeoutoffen
# 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
GET /api/v2/checks/{anfrage_id}

Volle Details. Beim ersten Aufruf wird Status gesendetgeoeffnet 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
POST /api/v2/checks/{anfrage_id}/offer

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.

⚠ Bitte immer 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

FeldTypPflicht?Beispiel
response_offer_idstringnein"ANG-2026-9911"
response_preis_nettostring (€/Jahr)empfohlen"1980.00"
response_laufzeit_jahrestringempfohlen"3"
response_kuendigungsfrist_monatestringnein"3"
response_wartungen_pro_jahrstringnein"2"
response_vollwartungbooleanneintrue
response_notrufbooleanneintrue
response_tuev_beglbooleanneintrue
response_tuev_pruefbooleanneinfalse
response_entstoerungbooleanneintrue
response_kommentarstring (max 4000)nein"Anfahrt inkl. Region Stuttgart"
pdf_base64string (Base64)★ dringend empfohlen"JVBERi0xLjQK…"
pdf_filenamestringmit pdf_base64"angebot.pdf"
# Response 200
{ "ok": true, "status": "angeboten", "channel": "api" }
🚀 Try it out
POST /api/v2/checks/{anfrage_id}/decline

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:

Webhook-Payload

POST mit JSON-Body. Header:

# 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

POST /api/v2/webhook-test

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):

FeldTypBeschreibung
anfrage_idstringEindeutige ID, Format PA-YYYY-XXXX
statusenumgesendet, geoeffnet, angeboten, abgelehnt, timeout
sent_atISO-8601Erstellt am
opened_atISO-8601 / nullErstmals abgerufen am
responded_atISO-8601 / nullBeantwortet/abgelehnt am
deadline_atISO-8601Antwortfrist
objekt_plz, _ort, _strassestringObjektadresse (Straße evtl. anonymisiert)
anzahl_aufzuegestringAnzahl Aufzüge (als String, da auch "2-3" möglich)
anlagenartstringz.B. "Personenaufzug, 8 Personen"
leistungsumfang_textmultiline stringFreitext-Soll-Beschreibung
vorliegendes_pdf_urlsignierte URL / nullBestands-Angebot zum Vergleich (falls Liftaro mitsendet) — Link läuft nach Stunden ab
response_*verschiedenEure Antwort, falls bereits abgegeben (read-only nach Submit)

Status-Codes

CodeBedeutungWann
200SuccessAlles ok
400Bad RequestJSON nicht parsbar oder Pflichtfeld fehlt
401UnauthorizedX-Partner-Key fehlt oder ungültig
403ForbiddenAnfrage gehört nicht zu eurem Partner-Account
404Not FoundAnfrage-ID existiert nicht
409ConflictStatus bereits final (angeboten oder abgelehnt)
410GoneAnfrage abgelaufen (timeout)
500Server ErrorBackend-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.