Speicher-API für die Wohnungssuche-Seite

Statt eines langen Links bekommt die Bewerbungsseite auf Wunsch einen kurzen: https://d-solve.de/apartment-search/<id>. Die Daten liegen dann auf dem Server und werden nach einer von dir gewählten Frist automatisch gelöscht. Diese Seite beschreibt die HTTP-Schnittstelle dahinter, für Menschen mit curl und für KI-Agenten. Der Baukasten nutzt dieselbe Schnittstelle.

KI-Agenten: Die vollständige Anleitung steht unter https://d-solve.de/projects/apartment-search/llms.txt. Maschinenlesbar: https://d-solve.de/apartment-search/api/openapi.json (OpenAPI 3) und der MCP-Server https://d-solve.de/apartment-search/mcp.

Die Idee

Kurzlink statt langem Link.

Ohne Speicherung steckt die ganze Seite im Link (siehe Dokumentation). Das ist privat und braucht keinen Server, aber die Links werden lang, Fotos müssen woanders liegen und der QR-Code wird dicht. Mit der Speicher-API legst du dieselben Parameter als JSON auf dem Server ab und bekommst eine kurze Adresse zurück.

DatenDieselben Parameter wie im Link, als JSON: {"loc":"Mainz",…}
POST /sitesDer Server speichert sie und gibt Kurzlink und Bearbeitungsschlüssel zurück.
KurzlinkBeim Öffnen setzt der Server die Daten in die unveränderte Seite ein.

Sicherheitsmodell

Harte Grenzen

Die Grenzen sind Kostenbremsen. Die Werte stehen an genau einer Stelle, in workers/apartment-search-api/config.json, und der Dienst liefert sie live unter GET /api/limits (dort unter today auch der Verbrauch des Tages). Kein Aufrufer kann sie überschreiten. Ein Agent ruft sie vor dem Anlegen ab. Tageskontingente gelten je UTC-Tag.

GrenzeFeld in /api/limitsAntwort bei Überschreitung
Seiten insgesamt (alle Nutzer)limits.maxSites503 capacity
Seiten je Konto (kostenlos / Premium)accounts.freeMaxSitesPerAccount, premium.maxSitesPerAccount409 account-limit
Fotos je Seitelimits.maxImagesPerSite409 image-limit
Größe je Fotolimits.maxImageBytes413
Bildspeicher insgesamtlimits.maxTotalImageBytes503 capacity
Daten je Seite (Bytes, Einträge, Zeichen je Wert)limits.maxDataBytes, limits.maxKeys, limits.maxValueChars400 / 413
Speicherfrist ohne Konto (Auswahl, Standard, Höchstwert)retention.options, retention.defaultDays, retention.maxDays400
Speicherfrist mit Premiumpremium.retentionOptions, premium.maxRetentionDays400
Neue Seiten je Absender und Taglimits.maxCreatesPerIpPerDay429 rate
Foto-Uploads je Absender und Taglimits.maxUploadsPerIpPerDay429 rate
Neue Seiten je Tag, alle zusammenlimits.maxCreatesPerDay, Verbrauch: today.creates503 daily-cap
Foto-Uploads je Tag, alle zusammenlimits.maxUploadsPerDay, Verbrauch: today.uploads503 daily-cap
Änderungen je Tag, alle zusammenlimits.maxUpdatesPerDay, Verbrauch: today.updates503 daily-cap
Bildabrufe aus dem Speicher je Tag (Cache-Treffer zählen nicht)limits.maxImageReadsPerDay, Verbrauch: today.imageReads503 daily-cap
Bild-Import per {url}: Zeitlimitfeatures.urlImport, limits.urlImportTimeoutMs422 / 502
Sitzung nach Anmeldung (Tage, gleitend)accounts.sessionDays–
Premium-Abo (Stripe-Preis)premium.available, premium.stripePriceId–

Außerdem begrenzt der Betreiber optional die Anfragen je Absender und Minute. Auch das meldet sich mit 429 und "code":"rate". Ob Funktionen aktiv sind, steht in features (cloud, accounts, premium, bulk, urlImport).

Endpunkte

API-Basis: https://d-solve.de/apartment-search/api. Alle Antworten sind JSON mit content-type: application/json, Zeitangaben sind Unix-Sekunden. Fehler haben die Form {"error":"…","code":"…"}; code gibt es nur bei einigen. Anfragen mit Inhalt senden content-type: application/json. Auth heißt Authorization: Bearer <Schlüssel>; die Konto-Endpunkte nutzen stattdessen das Session-Cookie. OPTIONS (CORS-Vorabfrage) antwortet mit 204.

MethodePfadAuthZweckAntwort
Allgemein
GET/–Übersicht mit Links200 {service, docs, openapi, limits, hint}
GET/health–Lebenszeichen200 {ok, service, time}
GET/limits–Funktionen, Grenzen, Fristen, Premium, Login-Anbieter, Adressen200 {service, project, features, limits, retention, premium, accounts, urls}
GET/openapi.json–OpenAPI-3-Beschreibung200 OpenAPI-Dokument
Seiten
POST/siteskeine (oder Session-Cookie)Seite anlegen. Body {data, retentionDays}201 Seite plus editKey (nur hier), apiUrl, note
GET/sites/:id–Seite lesen, wie der Link selbst öffentlich200 {id, url, subpages, data, images, imageCount, retentionDays, createdAt, updatedAt, expiresAt, owned}; 404, 410 abgelaufen
PUT (auch PATCH)/sites/:idSchlüssel oder Besitzerdata ersetzen (komplett, kein Zusammenführen) und/oder retentionDays neu setzen200 Seite wie oben, ohne images
DELETE/sites/:idSchlüssel oder BesitzerSeite samt Fotos löschen200 {ok, deleted}
POST/sites/:id/verifySchlüssel oder BesitzerSchlüssel prüfen200 {ok:true, via:"key"|"owner"}; 401, 403
POST/sites/:id/rotate-keySchlüssel oder BesitzerNeuen Schlüssel erzeugen; der alte wird ungültig200 {id, editKey, note}
Fotos
GET/sites/:id/images–Fotos der Seite200 {items:[{id, url, contentType, size, createdAt}]}
POST/sites/:id/imagesSchlüssel oder BesitzerFoto hochladen: Rohbytes mit content-type: image/*, oder multipart/form-data mit Feld file, oder JSON {"url":"https://…"} (der Server lädt das Bild)201 {id, url, contentType, size, imageCount, maxImages}; 409 image-limit, 413, 415
DELETE/sites/:id/images/:imageIdSchlüssel oder BesitzerFoto löschen200 {ok, deleted}
Konto, Premium (/auth, /me, /billing nur wenn features.accounts an ist, sonst 404)
GET/auth/providers–Konfigurierte Anbieter200 {providers:["google"]}
GET/auth/login?provider=…&return=…–Anmeldung starten302 zum Anbieter
GET/auth/callback/:anbieter–Rücksprung des Anbieters, nur für den Anbieter. Setzt das Cookie __Secure-as_session302 zu return#/auth/ok
GET/auth/meSession-CookieAngemeldeter Nutzer, Premium-Status, Kontolimits200 {id, name, provider, premium:{active, until, available, manageable}, limits:{maxSites, retentionOptions, maxRetentionDays, bulk}}
POST/auth/logoutSession-CookieDiese Sitzung beenden (in D1 widerrufen, Cookie löschen)200 {ok}
POST/auth/logout-allSession-CookieAlle Sitzungen dieses Kontos beenden200 {ok}
GET/me/sitesSession-CookieEigene Seiten200 {items:[{id, url, title, imageCount, updatedAt, expiresAt, expired}]}
POST/sites/:id/claimSession-CookieVorhandene Seite dem Konto zuordnen. Body {editKey}200 Seite; 403 Schlüssel falsch, 409 anderes Konto oder account-limit
POST/billing/checkoutSession-CookieStripe-Abo für Premium starten (Checkout im Modus subscription). Body {return}; Erfolg führt zu <return>#/konto/premium-ok, Abbruch zu <return>#/konto200 {url}
POST/billing/portalSession-CookieStripe-Kundenportal öffnen (kündigen, Zahlungsdaten ändern). Body {return}200 {url}
POST/billing/webhookStripe-SignaturZahlungsbestätigung, nur für Stripe200 {received}
POST/sites/bulkSession-Cookie, PremiumMehrere Seiten auf einmal. Body {retentionDays, items:[{data}]}. Nur wenn features.bulk an ist.201 {items, count}; 402 kein Premium

Die Seiten selbst

AdresseInhalt
https://d-solve.de/apartment-search/<id>Die Bewerbungsseite (HTML)
https://d-solve.de/apartment-search/<id>/aushang/Aushang mit denselben Daten
https://d-solve.de/apartment-search/<id>/mietunterlagen/Selbstauskunft und Unterlagen mit denselben Daten
https://d-solve.de/apartment-search/img/<datei>Hochgeladenes Foto (Dateiname aus der url der Upload-Antwort)

Eine abgelaufene oder gelöschte Seite zeigt eine kurze Hinweisseite (404). https://d-solve.de/apartment-search/ ohne ID führt zum Baukasten.

Ablauf mit curl

Der data-Teil besteht aus genau den Link-Parametern der Seite (alle Parameter): ein flaches JSON-Objekt Name → Text, unkodiert (kein %20). Mehrsprachige Texte haben das Suffix im Namen (headline.de). Leere Werte werden nicht gespeichert, data:-Adressen werden abgelehnt (Bilder hochladen). Für die Beispiele:

API=https://d-solve.de/apartment-search/api

1. Grenzen und Fristen abfragen (optional)

curl -sS "$API/limits"

2. Seite anlegen

curl -sS -X POST "$API/sites" \
  -H "content-type: application/json" \
  -d '{
    "data": {
      "langs": "de",
      "loc": "Mainz",
      "names": "Familie Beispiel",
      "headline": "Wir suchen ein Zuhause in Mainz",
      "subline": "Zwei Erwachsene und ein Kind suchen eine 3-Zimmer-Wohnung.",
      "email": "[email protected]",
      "phone": "-"
    },
    "retentionDays": 30
  }'

3. Antwort: Kurzlink und Schlüssel

{
  "id": "k7m2p9q4r6t8",
  "url": "https://d-solve.de/apartment-search/k7m2p9q4r6t8",
  "subpages": {
    "aushang": "https://d-solve.de/apartment-search/k7m2p9q4r6t8/aushang/",
    "mietunterlagen": "https://d-solve.de/apartment-search/k7m2p9q4r6t8/mietunterlagen/"
  },
  "data": { "langs": "de", "loc": "Mainz", … },
  "images": [], "imageCount": 0,
  "retentionDays": 30,
  "createdAt": 1790000000, "updatedAt": 1790000000, "expiresAt": 1792592000,
  "owned": false,
  "editKey": "as_…40 Zeichen…",
  "apiUrl": "https://d-solve.de/apartment-search/api/sites/k7m2p9q4r6t8",
  "note": "Der Bearbeitungsschlüssel wird nur dieses eine Mal angezeigt. …"
}

Den editKey jetzt sichern. Er kommt nie wieder. Für die nächsten Schritte:

ID=k7m2p9q4r6t8
KEY=as_…

4. Lesen

curl -sS "$API/sites/$ID"

Öffentlich, ohne Schlüssel. Liefert data, images, expiresAt. Der Schlüssel ist nie enthalten.

5. Ändern

curl -sS -X PUT "$API/sites/$ID" \
  -H "authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"data": { "langs": "de", "loc": "Mainz", "headline": "Neue Überschrift", "email": "[email protected]" }}'

data ersetzt den gesamten bisherigen Inhalt. Wer nur einen Wert ändern will, liest die Seite zuerst (Schritt 4), ändert das Objekt und schickt es vollständig zurück.

6. Foto hochladen

Drei Wege, alle mit dem Schlüssel; Anzahl und Größe begrenzt (limits.maxImagesPerSite, limits.maxImageBytes); erlaubt sind JPEG, PNG, WebP, GIF und AVIF.

# a) Rohbytes
curl -sS -X POST "$API/sites/$ID/images" \
  -H "authorization: Bearer $KEY" -H "content-type: image/jpeg" \
  --data-binary @wohnzimmer.jpg

# b) Formular mit Feld "file"
curl -sS -X POST "$API/sites/$ID/images" \
  -H "authorization: Bearer $KEY" -F "[email protected]"

# c) Der Server lädt ein öffentliches Bild selbst (für KI-Agenten)
curl -sS -X POST "$API/sites/$ID/images" \
  -H "authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"url":"https://upload.wikimedia.org/wikipedia/commons/…/bild.jpg"}'
{ "id": "…", "url": "https://d-solve.de/apartment-search/img/…jpg", "contentType": "image/jpeg", "size": 183422, "imageCount": 1, "maxImages": 20 }

7. Bild-URL in data verwenden

Die zurückgegebene url ist ein normaler Bild-Link und geht in jeden Foto-Parameter (photo, p1photo, p2photo, …; mehrere pro Kachel mit | getrennt). Die Seite ändert sich erst, wenn du die Daten schreibst:

curl -sS -X PUT "$API/sites/$ID" \
  -H "authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"data": { …alle bisherigen Werte…, "photo": "https://d-solve.de/apartment-search/img/….jpg" }}'

Fotos löschen: curl -sS -X DELETE "$API/sites/$ID/images/<imageId>" -H "authorization: Bearer $KEY". Die Bild-ID steht in der Upload-Antwort und in GET /sites/:id/images.

8. Frist abfragen und ändern

curl -sS "$API/sites/$ID" | grep -o '"expiresAt":[0-9]*'     # Unix-Sekunden, date -d @1792592000
curl -sS -X PUT "$API/sites/$ID" \
  -H "authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"retentionDays": 90}'

retentionDays muss eine der erlaubten Optionen sein (Schritt 1) und setzt die Frist neu ab jetzt. data und retentionDays lassen sich in einer Anfrage kombinieren.

9. Schlüssel prüfen und erneuern

curl -sS -X POST "$API/sites/$ID/verify"     -H "authorization: Bearer $KEY"
curl -sS -X POST "$API/sites/$ID/rotate-key" -H "authorization: Bearer $KEY"   # {"editKey":"as_…"}; der alte ist sofort ungültig

10. Löschen

curl -sS -X DELETE "$API/sites/$ID" -H "authorization: Bearer $KEY"

Löscht die Seite und alle ihre Fotos endgültig. Der Kurzlink zeigt danach eine Hinweisseite.

Fehlercodes

StatuscodeBedeutung und was zu tun ist
400–Anfrage ungültig: kein JSON, data leer oder kein Objekt, unzulässiger Parametername, Wert kein Text oder zu lang, data:-Adresse, retentionDays nicht erlaubt, url beim Bild-Import ungültig. Die Meldung in error nennt die Stelle.
401–Authorization: Bearer … fehlt, oder die Session (Cookie) ist nicht (mehr) gültig, oder ein Cookie-Schreibzugriff kam von einem Ursprung außerhalb von ALLOWED_ORIGINS.
402premiumFunktion nur mit Premium (/sites/bulk).
403–Schlüssel falsch, oder kein Zugriff auf diese Seite. Auch: return-Adresse nicht erlaubt.
404–Seite oder Foto gibt es nicht (oder nicht mehr), oder die Funktion ist abgeschaltet (Online-Speicherung, Konten, Bild-Import, Premium).
409image-limitDie Seite hat schon die Höchstzahl an Fotos. Erst ein Foto löschen.
409account-limitDas Konto besitzt schon die erlaubte Zahl an Seiten (frei 1, Premium 25).
409–claim: Die Seite gehört schon einem anderen Konto.
410–Die Seite ist abgelaufen und wird gelöscht. Neu anlegen.
413–Zu groß: Daten über 64 KiB, Anfrage über 512 KiB oder Foto über der Grenze.
415–Kein unterstütztes Bild (nur JPEG, PNG, WebP, GIF, AVIF).
422 / 504import-denied, import-not-found, import-not-image, import-timeoutBild-Import per {"url":…} gescheitert. Die Meldung nennt den Grund und einen Tipp für eine direkte Bildadresse. Google-Drive-Freigabelinks werden automatisch umgeschrieben.
429rateTageslimit für neue Seiten oder Uploads erreicht, oder zu viele Anfragen in kurzer Zeit. Später erneut versuchen oder den Link-Modus nutzen.
503capacitySeiten- oder Bildspeicher voll. Link-Modus nutzen.
503daily-cap: Tageskontingent des Dienstes aufgebraucht (Kostenbremse). Morgen wieder versuchen oder den langen Link ohne Speicherung verwenden.
502, 500–Bild konnte nicht von der url geladen werden, Anbieter oder Stripe nicht erreichbar, oder Serverfehler.

Für KI-Agenten

Wer Anfragen ausführen kann, kann die Seite selbst anlegen. Wer nicht, gibt dem Nutzer den fertigen curl-Befehl oder den langen Link. Alles zum Inhalt der Seite steht in der llms.txt; hier geht es nur um das Speichern. MCP-Clients nutzen statt roher HTTP-Aufrufe den MCP-Server.

Rezept

  1. Fragen: Kurzlink mit Speicherung (Daten auf dem Server, Löschung nach der Frist) oder langer Link ohne Speicherung (privat, nichts gespeichert)? Nicht still für den Nutzer entscheiden.
  2. Grenzen lesen: GET /api/limits. Ist features.cloud falsch oder die Anfrage scheitert, beim langen Link bleiben. Den Nutzer eine Frist aus retention.options wählen lassen (Standard retention.defaultDays).
  3. Daten bauen: dieselben Parameter wie im Link als JSON, unkodiert. Fotos zunächst weglassen oder bei öffentlichen Bild-Links belassen.
  4. Anlegen: POST /api/sites mit {data, retentionDays}. id, url, editKey aus der Antwort übernehmen.
  5. Fotos: für jedes Foto POST /api/sites/<id>/images mit {"url":"https://…"} (Server-Import). Die zurückgegebene url als Wert für photo/p1photo … in data setzen. Dann PUT /api/sites/<id> mit den vollständigen Daten.
  6. Prüfen: GET /api/sites/<id>, dann den Kurzlink öffnen oder dem Nutzer geben.
  7. Dem Nutzer melden: Kurzlink, Aushang- und Mietunterlagen-Link, Bearbeitungsschlüssel und die Frist als Datum (aus expiresAt).

Regeln

MCP-Server

Für Chat-Clients, die selbst keine HTTP-Aufrufe machen können (Claude, ChatGPT und andere MCP-Clients), gibt es dieselbe API als MCP-Server (Model Context Protocol): https://d-solve.de/apartment-search/mcp, Transport Streamable HTTP, JSON-RPC 2.0 per POST, ohne Anmeldung, ohne Sitzungen. Als Connector oder benutzerdefinierte Integration hinzufügen; die instructions des Servers verweisen auf die llms.txt.

Jedes Werkzeug ist genau einer der Endpunkte oben, durch denselben Handler, mit derselben Prüfung, denselben Grenzen und Fehlercodes. Der Bearbeitungsschlüssel ist ein Werkzeug-Argument (editKey) und wird intern zum Authorization-Header.

WerkzeugArgumenteEndpunkt
apartment_limits–GET /limits
apartment_createdata, retentionDaysPOST /sites
apartment_getidGET /sites/:id
apartment_updateid, editKey, data, retentionDaysPUT /sites/:id
apartment_deleteid, editKeyDELETE /sites/:id
apartment_add_imageid, editKey, urlPOST /sites/:id/images (URL-Import)
apartment_delete_imageid, editKey, imageIdDELETE /sites/:id/images/:imageId
apartment_rotate_keyid, editKeyPOST /sites/:id/rotate-key

Ergebnisse sind JSON, als Text und als structuredContent. Ein API-Fehler wird zum Werkzeug-Ergebnis mit isError: true und den Feldern status, error und code. Ausprobieren mit curl:

curl -sS -X POST https://d-solve.de/apartment-search/mcp -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Konto, Premium und Sammel-Anlage sind keine Werkzeuge; Agenten arbeiten mit dem Bearbeitungsschlüssel. GET /apartment-search/mcp antwortet mit 405 und einer kurzen Beschreibung, die OpenAPI-Beschreibung nennt den Server unter x-mcp.

Konto, Premium und Stripe

Ein Konto ist optional und nur für Premium oder mehrere Seiten nötig. Es gibt keine Passwörter, nur die Anmeldung mit Google.

Weiterführend