Putzkraftportal Anmelden
Für Entwickler

Partner-API

Für Vermieter-Software: Putzaufträge ins Portal geben und erfahren, was daraus wird. Putzkräfte brauchen keine Schnittstelle – sie arbeiten direkt im Portal.

Schnittstelle für Vermieter-Software: Reinigungsaufträge ins Putzkraftportal geben und erfahren, was daraus wird. Unsere eigene Plattform nutzt genau diese Schnittstelle (laravel/app/Portal/), es gibt keinen anderen Weg ins Portal.

Grundsätze

Endpunkte

Methode Pfad Zweck
GET / Version, eigener Partner
PUT /landlords/{id} Vermieter (= Abrechnungskonto): name*, email, phone, billing_name, billing_address, vat_id
PUT /units/{id} Objekt: landlord*, name*, place, address, latitude, longitude, clean_minutes, notes, description, size_sqm, bedrooms, bathrooms, beds, photo_url, parking
PUT /jobs/{id} Auftrag anlegen/ändern (s. u.) → 201 neu, 200 geändert
GET /jobs/{id} · /jobs?updated_since=&status=&landlord= Stand lesen (Abgleich)
POST /jobs/{id}/cancel absagen, Body {reason}. Unbekannter Auftrag = 200 {"result":"unknown"} – „Buchung gelöscht" darf blind durchgereicht werden
POST /jobs/{id}/rating {role: landlord|guest, rating: 1–5, comment} – je Rolle eine Bewertung, erneut senden überschreibt
POST /jobs/{id}/dismiss-cleaner Der Putzkraft wieder absagen, Body {reason} (steht in ihrer Mail). Nur solange der Job nicht läuft (409 already_started). Der Job geht zurück in die Auktion (ein Direkt-Job wird zur Auktion), die abgesagte Putzkraft sieht ihn nicht mehr. Löst job.released mit by: "landlord" aus.
PUT /jobs/{id}/planned Putztermin legen: {start, end} als JJJJ-MM-TTThh:mm. Das Portal rückt ihn ins Putzfenster und in die Arbeitszeit der Person, die putzt – job.planned in der Antwort sagt, wo er gelandet ist. Außerhalb des Fensters: 409 outside_window. Die Putzkraft bekommt eine Mail; zusätzlich job.rescheduled.
GET /jobs/{id}/messages Verlauf mit der Putzkraft: {open, messages: [{id, from: landlord|cleaner, author, body, sent_at, read_at}]}. Ihre Nachrichten gelten danach als gelesen (?peek=1 lässt sie ungelesen). Der Verlauf gehört zur Zusage EINER Putzkraft – nach einer Absage sieht die nächste ihn nicht.
POST /jobs/{id}/messages Nachricht an die Putzkraft: {body, author}201. Sie sieht sie beim Job im Portal und bekommt eine Mail. Ohne Zusage: 409 no_cleaner.
DELETE /jobs/{id}/messages/{mid} Nachricht aus dem Verlauf löschen – der Vermieter darf jede, die Putzkraft sieht sie danach nicht mehr. Löscht die Putzkraft eine eigene, kommt job.message_deleted mit message_id.
GET /landlords/{id}/messages?since= Alle Nachrichten zwischen diesem Vermieter und seinen Putzkräften, neueste zuerst (höchstens 500) – für einen gemeinsamen Posteingang beim Partner: wie …/jobs/{id}/messages, zusätzlich job, unit, cleaner. Liest nur; „gelesen“ setzt erst der Abruf des Verlaufs am Job.
GET /landlords/{id}/invoices?from=&to= Rechnungen der Putzkräfte an diesen Vermieter (seine Eingangsrechnungen): Nummer, Datum, Fälligkeit, cleaner, net/tax/gross, status (issued|paid|cancelled), cancellation, lines[{job, date, text, amount}] (job = portal_ref) und url zur Ansicht.
GET /landlords/{id}/pool eigene Putzkräfte – mit cleaner.availability (Wochenzeiten, Grenzen, Urlaub; gehört der Putzkraft, nur lesen)
PUT DELETE /landlords/{id}/pool/{email} einladen (Mail mit Annahme-Link) / entfernen
GET /landlords/{id}/availability?days=120 Tage, an denen mindestens eine angenommene eigene Putzkraft arbeitet: { "members": 1, "available": { "2026-09-24": ["anna@…"] } }. Damit entscheidet der Partner z. B., ob Gäste die Endreinigung buchen können
GET /units/{id}/nearby-cleaners?km=50 Registrierte Putzkräfte rund um das Objekt – nur wer auffindbar sein will und das Objekt im eigenen Umkreis hat. Liefert ref (z. B. c12), Name, Ort (nie die Adresse), Entfernung, Bewertung, „Über mich", erledigte Einsätze, cleaned_here, invitedkeine E-Mail, kein Telefon
GET /cleaners/{ref}/reviews Bewertungen mit Kommentaren (wer bewertet hat, bleibt ungenannt: „Vermieter"/„Gast", Monat), Verteilung nach Sternen
PUT /landlords/{id}/engagements/{eid} Putzkraft einladen: {cleaner: "c12" | E-Mail, unit?, from?, to?, price?, message?}. Ohne unit = alle Objekte, ohne from/to = dauerhaft. Idempotent; eine abgelehnte/beendete Einladung wird durch erneutes Senden neu verschickt
GET DELETE /landlords/{id}/engagements · …/{eid} Einladungen lesen (Kontaktdaten und Arbeitszeiten erst nach Annahme) / beenden (offene Direkt-Angebote gehen zurück in die Auktion, Zugesagtes bleibt)
GET /landlords/{id}/usage?month=JJJJ-MM erledigte Jobs, Freikontingent, Provision
PUT /webhook {url, secret} – Ziel für Ereignisse
GET /events?after={event_id} Ereignisse abholen (für Partner ohne erreichbare Adresse; auch als Abgleich)
POST /events/{id}/retry Zustellung sofort wiederholen

place, description, Größe/Zimmer/Betten, photo_url und die checklist (was zu tun ist) sieht jede Putzkraft schon vor der Zusage; address, notes, parking und access erst danach. parking = { "text": "Stellplatz 12 im Hof", "latitude": 47.2307, "longitude": 12.1811 } – Hinweis und/oder Punkt (beide Koordinaten oder keine). Die Putzkraft sieht beides nach der Zusage auf einer OpenStreetMap-Karte zusammen mit dem Objekt; null oder weglassen = keine Angabe. Ohne latitude/longitude sehen nur Stammkräfte den Auftrag – der Umkreis lässt sich sonst nicht prüfen.

PUT /jobs/{id}

{
  "landlord": "pno", "unit": "pno_1",
  "mode": "auction",
  "window": { "from": "2026-09-23T20:00", "to": "2026-10-18T21:00", "open_end": false },
  "auction_from": "2026-09-16",
  "price": { "start": 56.0, "max": 80.0, "currency": "EUR" },
  "clean_minutes": 90,
  "access": { "door_code": "4711", "valid_from": "2026-09-23T20:00", "valid_to": "2026-10-18T21:00", "hint": "Keypad links" },
  "checklist": [
    { "id": "task:12", "group": "Küche", "label": "Geschirrspüler ausräumen", "kind": "check" },
    { "id": "supply:tabs", "group": "Vorräte", "label": "Spülmaschinentabs", "kind": "count", "unit": "Stück", "target": 20, "current": 6 },
    { "id": "supply:seife", "group": "Vorräte", "label": "Flüssigseife", "kind": "choice", "current": "half",
      "options": [ { "value": "full", "label": "voll" }, { "value": "half", "label": "halb" }, { "value": "empty", "label": "leer" } ] },
    { "id": "n1", "group": "Abschluss", "label": "Schäden", "kind": "note", "optional": true }
  ]
}

Antwort job

{ "id": "pno-b83", "portal_ref": "j8", "landlord": "pno", "unit": "pno_1", "mode": "auction", "status": "claimed",
  "window": { "from": "2026-09-23T20:00", "to": "2026-10-18T21:00", "open_end": false },
  "auction": { "from": "2026-09-16", "phase": 3 },
  "price": { "start": 56, "max": 80, "current": 69.71, "claimed": 69.71, "currency": "EUR" },
  "cleaner": { "id": 1, "name": "Anna Huber", "phone": "+43 …", "email": "…", "rating": { "average": 4.8, "count": 12 },
               "billing_mode": "platform", "bank": null },
  "claimed_at": "…", "planned": { "start": "2026-09-24T08:00", "end": "2026-09-24T09:30" },
  "completed_at": null, "completion": null, "cancelled_at": null,
  "fee": { "rate": 0, "per_job": 2, "amount": 0, "free": true, "free_reason": "quota" }, "updated_at": "…" }

status: open · claimed · done · cancelled. cleaner.bank nur bei billing_mode: "private" (dann zahlt der Vermieter selbst). completion: { "results": [ { "id", "kind", "value" } ], "note": "…" }. fee ist vor dem Erledigen eine Vorschau.

Ereignisse (Webhooks)

POST an die hinterlegte Adresse, Body { "event_id", "type", "created_at", "data": { "job": {…}, … } }, Kopfzeilen X-Portal-Event, X-Portal-Event-Id, X-Portal-Signature: sha256=<HMAC-SHA256 des rohen Body mit dem Geheimnis>.

type zusätzlich in data Was der Partner typischerweise tut
job.claimed Türcode erzeugen und per PUT /jobs nachreichen, Gast informieren
job.started – (job.started_at) Die Putzkraft ist da und hat begonnen – Anzeige, ggf. Info an den Vermieter
job.assigned assigned_by (job.assignee) Die Putzkraft, die zugesagt hat, führt ein Team und hat den Job einer ihrer Putzkräfte gegeben (oder zurückgenommen). job.assignee = wer tatsächlich kommt ({id, name, phone} oder null), job.cleaner bleibt die Vertragspartnerin (Rechnung, Bewertung). Türcode/Rückfragen an assignee, wenn gesetzt.
job.released reason, late (< 48 h vor Beginn), released_by, ggf. by: "landlord" Türcode widerrufen, Vermieter informieren – außer bei by: "landlord": dann hat der Vermieter selbst abgesagt (POST …/dismiss-cleaner)
job.rescheduled – (job.planned) Termin anzeigen
job.updated / job.cancelled by: "operator" Der Betreiber hat den Job im Backoffice geändert (Fenster, Preise, Dauer, wieder geöffnet) bzw. abgesagt – Stand aus job übernehmen.
job.message message {id, from, author, body, sent_at} (job.messages.unread) Die Putzkraft hat dem Vermieter geschrieben – anzeigen, Vermieter benachrichtigen. job.messages.unread steht in jedem Job und zählt ihre Nachrichten, die noch nicht über GET …/messages gelesen wurden.
job.completed – (job.completion, job.fee) Ergebnisse einbuchen (Aufgaben, Vorräte), Bericht
job.declined reason, declined_by Direkt-Angebot abgelehnt – der Auftrag läuft jetzt in der Auktion
engagement.accepted / engagement.declined engagement Anzeige; bei Annahme stehen Kontaktdaten und Arbeitszeiten bereit
invoice.issued invoice {number, landlord, cleaner, issued_on, due_on, net, tax, gross, currency, cancels, url, jobs[]} Die Putzkraft hat dem Vermieter ihre Rechnung geschickt (auch per Mail). url zeigt die Rechnung samt Überweisungs-QR; cancels gesetzt = Storno-Rechnung
job.unfilled days_left Vermieter warnen (7 Tage vor Beginn ohne Zusage)
pool.accepted landlord, email, cleaner Pool-Anzeige
pool.left landlord, email Putzkraft ist nicht mehr feste Putzkraft (selbst beendet) – Pool-Anzeige

Zustellung mindestens einmal: Antwort 2xx = angenommen, sonst Wiederholung nach 1, 5, 30 min, 2 h, 12 h. Ereignisse können doppelt und verspätet kommen – anhand event_id idempotent verarbeiten und den Stand immer aus data.job nehmen, nicht aus dem Typ. Sicherheitsnetz: GET /events?after= oder GET /jobs?updated_since=.

Provision

Je Vermieter und Kalendermonat sind die ersten N erledigten Putzjobs frei (Tarif; Standard 3, fee.free_reason: "quota") – egal ob aus der Auktion oder selbst vergeben. Danach eine feste Gebühr je Job: plan.fee_per_job (Standard 2 €) für Auktions-Aufträge, plan.fee_per_direct_job (Standard 1 €) für Direkt-Aufträge (mode: "direct" – Einzeltermin oder aus einer Einladung); bei der Zusage festgeschrieben (fee.per_job). Ein Prozentsatz vom Preis (rate) ist im Standardtarif 0 und bleibt nur für Alttarife. Abgesagte und freigegebene Aufträge zählen nicht. Stand jederzeit über /landlords/{id}/usage. Das ist Absicht: Vermieter sollen einen Grund haben, ihre Putzkräfte ins Portal zu holen.