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.
- Basis-Adresse:
https://<portal>/api/partner/v1 - Code:
putzkraftportal/app/Http/Controllers/Api/PartnerApiController.php, Regeln inputzkraftportal/app/Domain/ - Tests, die das Verhalten festhalten:
putzkraftportal/tests/Feature/
Grundsätze
- Anmeldung:
Authorization: Bearer pp_…– Schlüssel je Partner, ausgestellt mitphp artisan portal:partner <kurzname>. - Idempotent: Der Partner vergibt alle Kennungen (
{id}in der Adresse: Buchstaben, Ziffern,. _ ~ -, höchstens 120 Zeichen). Derselbe Aufruf zweimal ändert nichts doppelt. Wiederholen nach einem Netzfehler ist immer gefahrlos. - Der Partner rechnet, das Portal vermittelt: Zeitfenster, Preisrahmen und Checkliste berechnet der Partner aus seinen Buchungen. Das Portal kennt keine Buchungen, Gäste oder Vorräte.
- Stabilität: Felder werden in v1 nicht umbenannt oder entfernt; neue können dazukommen (Unbekanntes ignorieren).
- Fehler immer als
{"error":{"code","message"}}; bei422 invalid_requestzusätzlichfields.
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, invited – keine 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 }
]
}
window: Zeit zwischen Abreise und nächster Anreise, Ortszeit. Ohne Folgegast:to: null/open_end: true.auction_from: Beginn der Auktion (bei uns: Anreisetag der Buchung). Die Zeit bis zum Fensterbeginn teilt sich in Drittel: 1 nur Stammkräfte (haben das Objekt schon geputzt oder gehören zum Pool), Startpreis · 2 alle Putzkräfte (ohne Entfernungsgrenze – wie weit sie fahren, entscheiden sie selbst), Startpreis · 3 der Preis steigt gleichmäßig bisprice.max. Wer zuerst zusagt, bekommt den Auftrag zum dann gültigen Preis.mode: "direct"+direct_cleaner(refaus der Suche oder E-Mail): Einzeltermin nur für diese Putzkraft, zum Startpreis. Sie muss registriert sein (sonst422 unknown_cleaner– erst einladen), eine vorherige Einladung braucht es nicht: Ihre Zusage ist die Annahme. Lehnt sie ab (job.declined), geht der Auftrag in die Auktion.- Einladungen wirken automatisch: Gibt es für Objekt und Abreisetag eine angenommene Einladung, wird ein als
auctiongesendeter Auftrag im Portal zum Direkt-Angebot an diese Putzkraft (mode: "direct", vereinbarter Preis inprice.current) – auch bei künftigen Aufträgen. checklist-Arten:check(Haken, Pflicht),count(Zahl),choice(Auswahl ausoptions),note(Text),photo. Mitdisplay: "slider"werden geordnete Skalen (choice, z. B. Füllstand voll … leer) und Mengen mittargetals Schieberegler gezeigt. Alles außercheckist freiwillig, wennoptionalnicht gesetzt ist.idfrei wählbar – sie kommt im Ergebnis zurück.- Nicht mitgeschickte Schlüssel
access/checklistbleiben, wie sie sind;nullleert sie. - Geändertes Fenster nach der Zusage: Das Portal benachrichtigt die Putzkraft – genau einmal je Änderung.
- Abgesagten Auftrag erneut senden = er geht wieder auf (Buchung wiederhergestellt). Erledigte Aufträge:
409 already_done.
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.