POST /v1/keys/trial — Self-Service-Trial-Key
Mintet ohne Registrierung einen befristeten Trial-API-Key (tier trial).
Der Request-Body ist optional und kennt zwei freiwillige Felder:
| Feld | Typ | Bedeutung |
|---|---|---|
source | Slug [a-z0-9_-], max. 64 | Kampagnen-/Kanal-Zuordnung (z.B. "dachgold", "landing"). Keine PII. |
email | E-Mail-Adresse | Kontaktadresse zur Key-Zuordnung. Optional, ungeprüft, kein Mailversand. |
Der Key wird genau einmal im Klartext zurückgegeben (Cache-Control: no-store)
und ist danach nicht wiederherstellbar — gespeichert wird nur SHA-256-Hash +
Prefix.
# PII-frei (DACHGOLD-Muster)
curl -X POST https://api.lodapi.de/v1/keys/trial \
-H "Content-Type: application/json" \
-d '{"source": "dachgold"}'
# mit Lead-Zuordnung (lodapi.de/key-Muster)
curl -X POST https://api.lodapi.de/v1/keys/trial \
-H "Content-Type: application/json" \
-d '{"source": "landing", "email": "konsti@scenerii.com"}'
Response (200):
{
"key": "lod_td8j5b86m6s32ujqbdmf8wwah5sv99xd",
"tier": "trial",
"expires_at": "2026-07-14T10:12:01Z",
"limits": { "requests_per_minute": 60, "requests_per_day": 5000 },
"scopes": ["read"],
"docs_url": "https://lodapi.de/docs"
}
- TTL: 7 Tage ab Mint. Abgelaufene Keys werden beim Lookup wie revoked behandelt (401 auf der Daten-API); die Row bleibt für Conversion-Analytics.
- Limits: 60 req/min, 5000 req/Tag — deklariert, Enforcement soft
(ADR-0014 „Quota = soft”): sichtbar über
X-Lodapi-Quota-Used/X-Lodapi-Quota-Limit(Budget = 7 × 5000 = 35 000). - Scope: read-only — die gesamte
/v1-Daten-API ist read-only, dasscopes-Feld dokumentiert den Contract. email(seit 2026-08-17, Konsti-Entscheidung „Mail gegen Key”): landet inlodapi_meta.api_key.contact_email— dieselbe Spalte wie beim Admin-Mint, dadurch ohne Extra-Code inGET /v1/admin/api-keyssichtbar. Keine Verifikation, kein Double-Opt-in, kein Versand aus diesem Endpoint. Ein Mint ohne Adresse liefert exakt denselben Key — die Adresse ist nie Voraussetzung. Die Antwort spiegelt sie nicht zurück (Response-Contract unverändert). Rechtsgrundlage/Speicherdauer/Löschweg:05_operations/trial-key-email-capture.md(Prüfauftrag Themis).- Abuse-Schutz auf dem Mint: max. N Keys pro IP pro 24 h
(
LODAPI_TRIAL_MINTS_PER_IP_PER_DAY) →429mitRetry-After-Header undretry_after-Member (Sekunden) im problem+json-Body. - Vom Keyless-Tageslimit ausgenommen (seit T-0079):
/v1/keys/**zählt nicht gegen die 1.000 keyless Requests pro IP und Tag und wird auch nie mit der Keyless-429 beantwortet. Der Weg zum Key darf nicht hinter dem Limit liegen, aus dem der Key herausführt — siehe Limits. - Kill-Switch:
LODAPI_TRIAL_KEYS_ENABLED=0→503. Clients sollen still degradieren (Angebot nicht zeigen, später erneut versuchen) — der Endpoint darf jederzeit ausfallen, ohne Consumer zu brechen. - Kontext: eingeführt für die DACHGOLD-Promo-Integration (Key als
„Take-away” nach dem ersten Spiel-Run, Konsti-Entscheidung „Key nach
erstem Wow, kein Gatekeeper”); generisch für jede Kampagne nutzbar.
Zweiter Consumer: die Self-Serve-Seite
lodapi.de/key(source: "landing").
Stolperdrähte
- Das Per-IP-Limit greift auf die echte Peer-Adresse, nicht auf
request.client.host: uvicorn läuft mit--forwarded-allow-ips "*"und liefert dort den erstenX-Forwarded-For-Eintrag — den kann der Client selbst schreiben, die Mint-Quote wäre per Header beliebig oft zurücksetzbar (T-0080, gefixt 2026-08-19). Gezählt wird der rechteste Eintrag, den Caddy anhängt; gemeinsamer Helperkeyless_limit.client_ip(), denselben nutzt das Keyless-Tageslimit. Regressionstesttests/test_trial_keys.py::TestMintIpIsSpoofSafefährt dafür den echten Prod-ASGI-Stack inklusiveProxyHeadersMiddleware— ohne die Schicht ist der Spoof im Test gar nicht nachstellbar und ein Test dagegen wertlos. - Hängt ein eigener Reverse-Proxy des Nutzers davor (statt Direkt-Call vom
Browser), zählt dessen Ausgangs-IP für alle seine Nutzer zusammen. CORS-Origins
sind offen (
*): Browser können direkt gegenapi.lodapi.deminten, ein Proxy ist nicht nötig. - CORS-Preflight: ein JSON-POST (
Content-Type: application/json) ist kein „simple request” — der Browser schickt vorherOPTIONS.POSTmuss dafür inallow_methodsder CORSMiddleware stehen (seit 2026-08-17, davor["GET", "OPTIONS"]→ jeder browser-seitige Mint scheiterte am Preflight mit400 Disallowed CORS method). curl merkt davon nichts — der Fehler ist nur im Browser sichtbar. Festgenagelt intests/test_trial_keys.py::test_cors_preflight_allows_browser_mint. Ein bodyloserPOSTohne Header bleibt preflight-frei und funktioniert auch ohne diese Einstellung — dann aber ohnesourceund ohneemail: ein Body brauchtContent-Type: application/json, und der macht den Request preflight-pflichtig. Lead-Capture aus dem Browser hängt damit am deployten CORS-Fix — es gibt keinen preflight-freien Weg, der die Adresse mitnimmt. - Kein
emailin der URL. Ein?email=-Query-Param wäre der naheliegende Trick, um den Preflight zu umgehen — er schreibt die Adresse aber in Caddy-Access-Logs undapi_key_usage.path, also an zwei Stellen ohne Löschkonzept. Bewusst nicht gebaut. - Zusätzliche, unbekannte Body-Felder werden von Pydantic still verworfen
(kein
extra="forbid") — ein Tippfehler wiemailstattemailfällt also nicht als 422 auf, die Adresse ist dann einfach weg. sourceist ein Slug ([a-z0-9_-], max. 64) — alles andere gibt 422. Eine syntaktisch kaputteemailgibt ebenfalls 422 (kein Key wird geminted).
Limit-Stand (2026-08-19): Code-Default 3 Mints/IP/24 h; prod läuft auf 10 (
LODAPI_TRIAL_MINTS_PER_IP_PER_DAY=10incode/prod/.env, Decision 2026-08-17-06). Frontend nennt bewusst keine Zahl.