Raw Markdown · rankion-api-reference.md
Stand: 2026-09-09 · Version: v1 · 562 Endpoints
Single Source of Truth für die komplette REST-API. Diese Datei wird via CLAUDE.md auto-importiert und serviert /api-reference + /api-docs/markdown.
Vollständigkeits-Gate: Jeder
/v1-Route MUSS hier eine Zeile haben — erzwungen durchphp artisan api:doc-coverage(hart im Deploy: Pipeline-Stagetest:staticin.gitlab-ci.yml+ server-seitig indeploy/release.sh[5/10] vor der OPcache-Aktivierung, analog zuschema:lint/mcp:catalog-coverage). Neuer Endpoint ohne Doku-Zeile ⇒ Deploy bricht ab. Skelett-Zeilen:php artisan api:doc-coverage --emit.
# Token im Frontend erstellen: /settings/api-tokens
TOKEN="<dein-sanctum-token>"
BASE="https://rankion.ai/api/v1"
curl -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
"$BASE/projects"
Alle Endpoints sind Team-scoped via auth:sanctum Middleware. Cross-Team-Zugriff → 403.
keyword_id, article_id etc. zum Pollen.{message, errors:{field:[reason]}}Jobs/Aktionen mit credits:N Middleware ziehen N Credits aus dem Team-Balance vor Job-Dispatch. Bei cache=false Refresh (Explorer) wird inline 50 Credits abgebucht.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /tracking-projects |
alle Team-Projekte | — |
| POST | /tracking-projects/analyze |
Wizard Step 1+2: Domain/Keyword analysieren, dispatcht AnalyzeDomainJob. Body: {mode:"domain"|"keyword", domain?, keyword?, name?, language?, country?, own_domains[]?} → 202 + project_id |
— |
| POST | /ai-visibility/ingest |
Guided Prompt-Ingest (skill/agent/MCP front door). Schickt eine Prompt-Liste; State-Machine status: needs_input (fragt domain+market) → analyzing → needs_confirmation → activated. Body: {prompts[], domain?/brand?, language?, country?, project_id?, confirm?, auto_confirm?, platforms?, frequency?, start_run?}. Orchestriert analyze→activate→classify; team-scoped, idempotent (team,domain). Doku: docs/modules/ai-visibility-ingest.md |
analyze 5 / activate 0 |
| GET | /ai-visibility/ingest/{id} |
Poll des Ingest-/Analyse-Status (gleiche Envelopes). Team-scoped (403 cross-team) | — |
| GET | /tracking-projects/{id} |
Detail mit KPIs | — |
| PUT | /tracking-projects/{id} |
Settings: {name?, brand_name?, brand_aliases[], competitor_domains[], own_domains[], tracking_frequency?, llm_platforms[], llm_with_search?, llm_without_search?, track_aio?, reality_check_enabled?} — llm_platforms[] erlaubt: chatgpt, perplexity, claude, gemini, copilot, grok, google_ai_mode — own_domains = weitere eigene Domains (Full-Replace, normalisiert, schließt Hauptdomain aus, reklassifiziert Cited Sources sofort) |
— |
| DELETE | /tracking-projects/{id} |
⚠ Projekt samt aller Daten löschen (Keywords, Prompts, Runs, Scores, Cited-Sources, LLM-Results + alle CASCADE-Children). Irreversibel. Team-gescoped (403 cross-team). → 204 No Content | nur eigenes Team · 0 |
| GET | /tracking-projects/{id}/analysis-status |
Wizard polling: {analysis_status, analysis_phase, analysis_error, suggested_keywords, suggested_competitors, competitor_discovery, competitor_rediscovery_status, competitor_rediscovery_error, suggested_prompts, suggested_personas, classification}. competitor_discovery unterscheidet ok | empty | failed; die beiden rediscovery-Felder tragen den Lebenszyklus von POST .../competitors/discover |
— |
| POST | /tracking-projects/{id}/activate |
Wizard Step 5 atomic: erstellt TrackingKeyword/Prompt/Competitor + flippt status=active. Body: {keywords[], competitors[], prompts[], platforms[], frequency, with_search?, without_search?, track_aio?, reality_check_enabled?, personas[]} — platforms[] erlaubt: chatgpt, perplexity, claude, gemini, copilot, grok, google_ai_mode |
— |
| POST | /tracking-projects/{id}/run |
Tracking-Run starten (async) | — |
| GET | /tracking-projects/{id}/runs |
Run-Historie (?limit=&offset=) |
— |
| POST | /tracking-projects/{id}/sync-sources |
Cited-Sources sync mit externem Tool (refresht zusätzlich in_portfolio-Flags via SyncPortfolioDomainsJob) |
— |
| POST | /tracking-projects/{id}/own-domains |
Domain als eigene übernehmen (Claim, Pendant zur Cited-Sources-Hover-Aktion). Body: {domain} — entfernt sie ggf. aus den Wettbewerbern, reklassifiziert bestehende Citations sofort. Returnt {domain, own_domains[]}; 422 bei ungültiger Domain |
— |
| GET | /ai-visibility/{id}/business-profile |
Business-Profil (Geo-Scope, Standort, Einzugsgebiet, Zielgruppen-Linse mit likely_queries) — {profile: null} solange keins gebaut |
— |
| PATCH | /ai-visibility/{id}/business-profile |
Profil korrigieren. Body: {geo_scope?, city?, service_area_cities[]?} — 422 ohne vorhandenes Profil; synct Legacy-Spalten |
— |
| POST | /ai-visibility/{id}/business-profile/refresh |
Profil neu analysieren (Multi-Page-Crawl + Klassifikation, async). 202; Poll via GET refresh_status |
5 |
| GET | /ai-visibility/{id}/article-requests |
Partner-Outreach-Anfragen des Projekts (?per_page=) |
— |
| POST | /ai-visibility/{id}/article-requests |
Artikel-Platzierung beim Partner anfragen. Body: {domain, prompts[], keywords[], message?, consent:true}. 422 ohne consent, 409 wenn bereits aktive Anfrage für die Domain |
0 |
| DELETE | /ai-visibility/{id}/article-requests/{requestId} |
Anfrage-Status entfernen (status=withdrawn) | — |
| GET | /tracking-projects/{}/keyword-suggestions |
Listet Sitemap-basierte Keyword-Vorschläge, sortiert nach Suchvolumen, paginiert | — |
| POST | /tracking-projects/{}/prompts/bulk-classify-intent |
Klassifiziert alle unklassifizierten Prompts asynchron nach Funnel-Stage (202) | — |
| POST | /tracking-projects/{}/prompts/bulk-monitor |
Setzt Monitor-Status für mehrere Prompts per ID-Liste | — |
| GET | /tracking-projects/{}/prompts/cost-preview |
Schätzt monatliche Monitoring-Kosten für aktive/ausgewählte Prompts | — |
| POST | /tracking-projects/{}/prompts/refresh-volumes |
Aktualisiert Prompt-Suchvolumina asynchron per Job (202) | — |
| POST | /tracking-projects/{}/prompts/{}/classify-intent |
Klassifiziert einzelnen Prompt synchron nach Funnel-Stage | — |
| PATCH | /tracking-projects/{}/prompts/{}/monitor |
Setzt Monitor-Status eines Prompts (active/paused/excluded) | — |
| GET | /tracking-projects/{}/sitemap-pages |
Listet gescannte Sitemap-Seiten des Projekts paginiert | — |
| POST | /tracking-projects/{}/sitemap-scan |
Startet asynchronen Sitemap-Scan, liefert entdeckte Sitemap-URL (202) | — |
Ein TrackingProject an einen anderen bestehenden Rankion-Nutzer (auch in einem anderen Team) mit Lese- UND Schreibzugriff freigeben. Läufe, die ein Gast startet, belasten das Eigentümer-Team. Auth ist pro Endpoint asymmetrisch (Eigentümer vs. Eingeladener) — siehe „Auth"-Spalte.
| Method | URL | Beschreibung | Auth · Credits |
|---|---|---|---|
| GET | /tracking-projects/{id}/shares |
Freigaben eines Tracking-Projekts auflisten (Status, eingeladene E-Mail, Berechtigung) | nur Eigentümer · 0 |
| POST | /tracking-projects/{id}/shares |
Freigabe anlegen (Einladung). Body: {email} — lädt einen bestehenden Nutzer mit permission:"write" ein. Idempotent (reaktiviert abgelaufene/terminierte Freigaben auf derselben Zeile). Stellt via „Mit mir geteilt" + E-Mail (TrackingProjectShared) zu |
nur Eigentümer · 0 |
| DELETE | /tracking-projects/{id}/shares/{share} |
Freigabe widerrufen (status→revoked) |
nur Eigentümer · 0 |
| GET | /shared-with-me |
Mit dem authentifizierten Nutzer geteilte Projekte (Gast-Sicht) | jeder · 0 |
| POST | /tracking-project-shares/{share}/accept |
Freigabe annehmen (E-Mail-Match, atomarer lockForUpdate-Consume → status→active) |
nur Eingeladener · 0 |
| POST | /tracking-project-shares/{share}/decline |
Freigabe ablehnen (status→declined) |
nur Eingeladener · 0 |
| DELETE | /tracking-project-shares/{share} |
Freigabe entkoppeln/verlassen (status→detached) |
nur Eingeladener · 0 |
| Method | URL | Notes |
|---|---|---|
| GET | /tracking-projects/{id}/keywords |
?active_only=true |
| POST | /tracking-projects/{id}/keywords |
Body: {keyword, keyword_group?, tags?, search_volume?, cpc?, difficulty?, search_intent?, language?, country?, source?} · source ∈ manual|sitemap|suggestion|imported (Plan #87); is_active wird nicht validiert und daher ignoriert |
| PUT | /tracking-keywords/{id} |
|
| DELETE | /tracking-keywords/{id} |
|
| GET | /tracking-projects/{id}/prompts |
|
| POST | /tracking-projects/{id}/prompts |
Body: {prompt_text, prompt_category?, persona_label?, commercial_intent?} |
| PUT | /tracking-prompts/{id} |
|
| DELETE | /tracking-prompts/{id} |
|
| POST | /tracking-projects/{id}/prompts/extract |
Gibt ALLE (oder Filter-Subset) Prompts des Projekts im Response zurück (Backup/Export → Excel/CSV) UND löscht sie atomar — 1 Call statt N Einzel-DELETEs. Filter (optional, AND): prompt_ids[], active_only, category. → {extracted[], deleted_count} |
| POST | /tracking-projects/{id}/prompts/import |
CSV/Excel Bulk-Import. multipart/form-data: file, optional mapping[col_name]=index. → {imported, skipped, errors, mapping_used} |
| POST | /tracking-projects/{id}/prompts/{promptId}/classify-persona |
Persona EINES Prompts per AI setzen (constrained auf target_audiences[].label, sync). → 200 + {tracking_prompt_id, persona_label, classified_at}. is_user_edited-Personas geschützt · 0 |
| POST | /tracking-projects/{id}/prompts/bulk-classify-persona |
Alle Prompts ohne persona_label projektweit klassifizieren (dispatcht BackfillPersonaJob, generiert ggf. zuerst Personas, async). → 202 + {tracking_project_id, pending_classifications, status:"queued"} · 0 |
Prompt-Liste mit mehrdimensionalem Filter — GET /tracking-projects/{id}/prompts akzeptiert: ?search= (Volltextsuche auf prompt_text, MariaDB-FULLTEXT Boolean-Mode: mehrere Wörter = UND, +pflicht -ausschluss "exakte phrase"; Substring-Fallback bei <3-Zeichen-Termen), ?tags[]= (Tag-IDs; OR innerhalb Dimension, AND zwischen Dimensionen), ?category[]= (string ODER array, back-compat), ?intent[]=, ?funnel[]=, ?persona[]=, ?language[]=, ?monitor_status[]=, ?volume_min=, ?active_only=1, ?per_page= (paginiert sonst flache Liste).
Alle Ressourcen team-gescoped (Fremd-Team → 404). 4 System-Dimensionen (topic/competitor/sub_intent/campaign) pro Team auto-geseedet.
| Method | URL | Notes |
|---|---|---|
| GET | /tag-dimensions |
Team-Dimensionen (+tags_count) · 0 |
| POST | /tag-dimensions |
{key:/^[a-z0-9_]+$/, label, color?, icon?} → 201 · 0 |
| PATCH | /tag-dimensions/{id} |
{label?, color?, icon?, sort_order?} · 0 |
| DELETE | /tag-dimensions/{id} |
System-Dimension → 422 · 0 |
| GET | /prompt-tags |
?dimension={id} optional · 0 |
| POST | /prompt-tags |
{dimension_id, label, color?} — slug-dedupe → 201 · 0 |
| PATCH | /prompt-tags/{id} |
{label?, color?, description?} (label → neuer slug) · 0 |
| DELETE | /prompt-tags/{id} |
aktive Assignments vorhanden → 422 (erst mergen/lösen). Soft-Delete · 0 |
| POST | /prompt-tags/{id}/merge |
{target_id} — Assignments umhängen+dedupen, Quelle soft-delete · 0 |
| POST | /tracking-prompts/{id}/tags |
{tag_id} zuweisen → 201 · 0 |
| DELETE | /tracking-prompts/{id}/tags/{tagId} |
Zuweisung entfernen · 0 |
| POST | /tracking-projects/{id}/prompts/bulk-tag |
{prompt_ids[≤500], tag_id, operation:add|remove} → {updated, skipped} · 0 |
| POST | /tracking-projects/{id}/prompts/apply-single-tag |
{tag_id} — EINEN vorhandenen Tag im Hintergrund auf alle Prompts des Projekts anwenden (additiv: legt nichts an, aendert keine Zuweisung). Nicht KI-faehige Dimension → 422. → 202 {id, status, tag_id, poll_url} + Location · 5 |
| GET | /tracking-projects/{id}/prompts/taxonomy-run/{runId} |
Lauf-Status: {status, scope_tag_id, single_tag, proposed_count, is_running, is_single_tag_run}. Fremder Lauf → 404 · 0 |
| Method | URL | Notes |
|---|---|---|
| GET | /tracking-projects/{id}/cited-sources |
?domain=&outreach_status=&is_own_domain=&sort=&per_page=. Zeile enthält in_portfolio (bool, „Anfrage möglich"), citation_possibility_score (int, persistierter Floor 95 für Portfolio-Domains) + effective_possibility (int, identischer Read-Time-Wert) |
| GET | /tracking-projects/{id}/cited-sources/export |
CSV-Stream. ?from=&to=&status=&platform=&search=&sort= |
| POST | /tracking-projects/{id}/cited-sources/analyze |
Citation-Analyse dispatchen. Body optional {source_ids:[]}. Default: alle unanalyzed. → 202 + {queued, estimated_seconds} |
| PATCH | /tracking-projects/{id}/cited-sources/{source_id} |
Body: {outreach_status?:neu|anfrage_gestellt|verlinkt|abgelehnt|ignoriert, notes?} |
| Method | URL | Notes |
|---|---|---|
| GET | /tracking-projects/{id}/scores |
Score-Historie (?from=&to=) |
| GET | /tracking-projects/{id}/platform-breakdown |
(?from=&to=) |
| GET | /tracking-projects/{id}/insights/low-hanging-fruit |
(?days=30&min_possibility=60&limit=5) |
| GET | /tracking-projects/{id}/insights/platform-gap |
(?days=30) |
| POST | /tracking-projects/{id}/insights/platform-gap/{platform}/diagnose |
async — 1 Credit |
| GET | /tracking-projects/{id}/insights/potential-hero |
{eligible:false} wenn <20 Abfragen |
| GET | /tracking-projects/{id}/off-page-readiness |
Off-Page-/Demand-Achsen-Score — 0 Credits, team-scoped. Returns {score:int|null, data_available, components:{geo.offpage.brand_demand, geo.offpage.third_party_mentions, geo.offpage.source_affinity → {score, data_available, meta, weight}}, meta}. score=null=noch keine Daten (≠0). 403 Fremd-Team, 404 unbekannt. SSOT OffPageReadinessScorer (bridged community_project_id ↔ tracking_project_id). Modul: geo-criteria-catalog |
| POST | /tracking-projects/{id}/prompts/{prompt}/generate-brief |
Content-Brief via Claude — 3 Credits |
| POST | /tracking-projects/{id}/brand-aliases |
Body: {alias} |
| GET | /tracking-projects/{id}/competitors |
Gemeinsame Wettbewerberliste des Projekts. Returnt {competitors:[{domain, brand_name, source, source_url, is_manual, is_active}]}, nach Domain sortiert — inklusive der abgewaehlten (is_active:false), damit ein Aufrufer die Abwahl sieht statt eine Luecke zu raten. source: wizard | api | analysis | null (von Hand). Reicher als das flache competitor_domains aus GET /tracking-projects/{id} |
| PATCH | /tracking-projects/{id}/competitors |
Body: {domains[]} (max 50) — normalisiert/dedupliziert, synct competitor_domains + TrackingCompetitor. Returnt {competitor_domains[]}. Powert das Gap-Wettbewerber-Modal |
| POST | /tracking-projects/{id}/competitors/discover |
Wettbewerber neu ermitteln lassen — EIN OpenAI-Aufruf mit Websuche, ohne den ganzen Einrichtungslauf. Body optional {limit} (1-30, Vorgabe 10). 202 {status:'queued', tracking_project_id, credits_required, poll_url}; 503 discovery_unavailable, wenn kein OpenAI-Zugang eingerichtet ist. Fortschritt ueber GET /tracking-projects/{id}/analysis-status → competitor_rediscovery_status (queued | running | completed | failed) und competitor_discovery (ok | empty | failed — empty heisst gesucht und nichts gefunden, failed heisst nicht suchen koennen). Das Ergebnis ist ein Vorschlag in suggested_competitors; die bestaetigte Liste bleibt unangetastet, bestaetigt wird ueber PATCH .../competitors. Abgezogen wird NACH Erfolg — 5 Credits |
Surface der Phase-0-Fact-Check-Engine: jeder LlmResponseClaim-Befund (Abweichung
zwischen LLM-Aussage und vouchter Wahrheitsquelle) wird hier gelistet, aufgelöst und
re-verifiziert. Alle Routen team-/share-scoped; {claim} ist nested-IDOR-validiert
(Claim muss zum {id}-Projekt gehören → sonst 404). Seed/Refresh crawlen NUR die
Eigen-Domains des Projekts (SSRF-restricted). Kosten-amplifizierende Routen: throttle:5,1.
| Method | URL | Notes |
|---|---|---|
| GET | /tracking-projects/{id}/accuracy |
Claim-Liste. Filter ?status=&severity=&platform=&resolution_status=&claim_type=&resolution_reason=&search= (search = claim_normalized LIKE; claim_type ∈ price|duration|feature_availability|technical_spec|history|rating|other; resolution_reason ∈ RESOLUTION_REASONS — z.B. die „Zu bewerten"-Lane via resolution_reason=inconclusive), sortiert Severity (critical→low) + last_seen_at. Paginiert ?per_page= (hard cap 100), eager-load atom.llmResult. Item-Shape via toAccuracyPayload() (inkl. resolution_status, ground_truth_source_anchor{tier,source,passage}, response_excerpt). 403 Fremd-Team. — 0 |
| POST | /tracking-projects/{id}/accuracy/{claim}/resolve |
Body: {resolution_status: resolved|false_positive}. Setzt resolved_by_user_id + resolved_at. 404 wenn {claim} nicht zu {id} gehört (IDOR-Cross-Check). Returnt das aktualisierte Payload. — 0 |
| POST | /tracking-projects/{id}/accuracy/reverify |
On-demand Re-Verify offener+stale Claims (ReverifyProjectClaimsJob, isolierte ai-tracking-Queue, ShouldBeUnique). Per-Projekt RateLimiter 1/h → 429 wenn zu früh; 402 bei zu wenig Credits; sonst 202 + {data:{status:"queued", tracking_project_id, message}}. credits:5 (Gate; Abbuchung im Job nur bei ≥1 neu geprüftem Claim) + throttle:5,1 — 5 |
| POST | /tracking-projects/{id}/accuracy/backfill |
Erst-Prüfung des bestehenden Antwort-Korpus: fächert pro Claim-Atom-ohne-Claim einen ExtractAndVerifyClaimJob (ai-factcheck-Queue) auf — der Observer ist forward-only und reverify ist auf einem frisch-armierten Projekt ein No-op, daher erzeugt dieser Endpoint die ERSTEN Claims (BackfillProjectClaimsJob, isolierte ai-tracking-Queue, ShouldBeUnique, Cap brand_accuracy.fact_check_cap_per_run=500). Per-Projekt RateLimiter 1/h → 429; sonst 202 + {data:{status:"queued", tracking_project_id, message}}. throttle:5,1. Kostenlos (kein Credit-Gate; die Cross-Checks werden nicht hier abgerechnet) — 0 |
| POST | /tracking-projects/{id}/truth-source/seed |
Befüllt extracted_facts aus Brand-Profil (brand_name/aliases/usps/products/profile/own_domains) + optional ?discover_website=true (Eigen-Domain-Crawl, SSRF-safe). Quelle bleibt unreviewed (User muss bestätigen → armt erst dann das Gate). 202 + {data:{status:"queued", truth_source_id, facts_count, human_reviewed, message}}. throttle:5,1 — 0 |
| POST | /tracking-projects/{id}/truth-source/refresh |
Re-Crawl des Website-Tiers — Eigen-Domains ONLY (SSRF). 202 + {data:{status:"queued", truth_source_id, message}}. throttle:5,1 — 0 |
| GET | /tracking-projects/{id}/brand-kb |
Auto-KB Coverage-Status der Eigen-Domain-Webseiten-KB (brand_kb_passages). Flat-Shape (v1): {pages, passages, last_crawled_at, verifiable_pct, indexing}. verifiable_pct = Anteil Claims mit verification_status ∈ {true,false,outdated,partial} an allen Claims (identische Formel wie das accuracyCoverage-Computed). indexing liest seit Plan #105 A3b den GESPEICHERTEN Lauf-Zustand (kb_run_status in queued/running, innerhalb der Verwaist-Frist KB_RUN_STALE_AFTER) statt der alten Formel „KB leer + Projekt active" — die blieb true, auch wenn nie ein Crawl lief, und ein Polling darauf lief unbegrenzt. Ein Projekt ohne je gemessenen Lauf (NULL) faellt auf das alte Verhalten zurueck. 403 Fremd-Team. — 0 |
| POST | /tracking-projects/{id}/brand-kb/rebuild |
Eigen-Domain Re-Crawl + Re-Embed der Webseiten-KB. Der BrandKbRunStarter waehlt den Job: RefreshBrandKbJob bei vorhandener Wissensbasis (Diff + gezielte Nachpruefung), BuildBrandKbJob bei leerer. Beide auf der eigenen Queue crawl (nicht ai), Redirects DISABLED + assertSafeUrl. Hash-Dedup (UNIQUE(project, passage_hash)) macht den Re-Build idempotent. 202 + {data:{status:"queued"|"already_running", tracking_project_id, message}} — already_running heisst: es laeuft bereits ein Lesevorgang, es wurde NICHTS eingereiht (Plan #105 A4c; vorher schwieg die Route darueber und der Aufrufer bekam eine Zusage ohne Lauf). throttle:5,1. Das Lesen der eigenen Webseite ist kostenlos; die anschliessende Nachpruefung laeuft mit charge=true und wird wie accuracy/reverify abgerechnet — kein Credit-Gate auf der Route — 0 |
| GET | /tracking-projects/{id}/kb-refresh |
Auto-KB Auffrisch-Häufigkeit (Freshness-Loop): effektive Kadenz + deren Quelle + zuletzt eingelesen. {data:{effective_frequency, override, team_default, source, last_crawled_at}}. source = override (pro-Projekt gesetzt) > team (Team-brand_kb_refresh_default-Settings-Row existiert) > config (hard-coded Default weekly). effective_frequency/team_default ∈ daily|weekly|biweekly|monthly|off; override = der gesetzte Wert oder null (= erbt). last_crawled_at = jüngster brand_kb_passages.crawled_at. team-/share-scoped (authorizeTrackingProject → 403 Fremd-Team) — 0 |
| PATCH | /tracking-projects/{id}/kb-refresh |
Setzt die Pro-Projekt-Häufigkeit. Body: {frequency: daily|weekly|biweekly|monthly|off|null} (present+nullable) — null löscht den Override (erbt Team-/Config-Default). Validiert gegen BrandKbRefreshFrequency::values() → 422 bei ungültigem Wert. Returnt das identische GET-Payload (kbRefreshPayload). team-/share-scoped. throttle:5,1 — 0 |
| GET | /team/kb-refresh-default |
Team-Standard-Häufigkeit für die Auto-KB-Auffrischung. {data:{frequency: daily|weekly|biweekly|monthly|off}} — Team::getBrandKbRefreshDefault(); fällt auf config('brand_accuracy.kb_refresh_default') (weekly) zurück, wenn keine Settings-Row existiert. Scope: currentTeam() — 0 |
| PATCH | /team/kb-refresh-default |
Setzt den Team-Standard. Body: {frequency: daily|weekly|biweekly|monthly|off} (required, kein null). Validiert gegen BrandKbRefreshFrequency::values() → 422 sonst; Team::setBrandKbRefreshDefault() (Settings-Row). Returnt {data:{frequency}}. Scope: currentTeam(). throttle:5,1 — 0 |
| GET | /brand-knowledge |
Chat-Erdung der eigenen Marke: team-scoped Faktencheck-Paket in EINEM read-only Call. Auflösung der Marke via ?brand= (id/name/domain) bzw. Auto-Single, sonst disambiguation. Query: ?brand=&q=&top_k=&include=. q = KB-Query (RAG, top_k ∈ 1..10, Default 5); include = CSV-Subset aus kb,facts,truth (Default alle). FLAT-Shape: {brand{id,name,domain}, armed, kb_status, kb_passages[], fact_errors[], truth_sources[], disambiguation[]} (kb_passages mit source_url-Beleg) bzw. {brand:null, disambiguation[], message}. Liest BrandKnowledgeService (KB-Passagen + Claim-Verdicts + kuratierte Truth-Sources), kein Re-Crawl (dafür brand-kb/rebuild). Kuratierter Skill ai-visibility.brand-knowledge. auth:sanctum, throttle:60,1. 403 Fremd-Team. — 0 |
| Method | URL | Notes |
|---|---|---|
| GET | /tracking-projects/{id}/avi |
neuester AVI-Score, tier (red/yellow/green), 6-Dim-Scores, recommendations. 404 wenn kein Run |
| GET | /tracking-projects/{id}/dimensions |
alle 6 Dimension-Scores pro Plattform |
| POST | /tracking-projects/{id}/reality-check-report |
PDF-Generierung async. 404 wenn kein Run |
| GET | /tracking-projects/{id}/reality-check-report/status |
Polling-Endpoint für die laufende Report-Generierung: {status: queued|running|ready|failed, progress?, download_url?} |
User-Prompt geht parallel an ausgewählte LLMs (ChatGPT, Perplexity, Claude, Gemini, Copilot, Grok) je mit/ohne Web-Search. Live-Card-Grid in der UI, persistente Runs, Sentiment + Brand-Extract pro Antwort.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /prompt-check |
Liste runs des Teams. Filter: ?project_id= · ?status=pending|running|completed|partial|failed · ?search= (Volltext auf prompt+title) · ?search_responses= (Volltext im response_text) · ?sentiment=positive|negative|neutral|not_mentioned (≥1 Response) · ?platform=chatgpt|perplexity|claude|gemini|copilot|grok (≥1 Response) · ?per_page=25&page=1 |
— |
| POST | /prompt-check |
Neuer Run async (202). Body: {tracking_project_id, prompt (5-5000ch), platforms:["chatgpt","claude",…], with_search?:{platform:bool}, title?} — freie Teilmenge (min 1) aus: chatgpt, perplexity, claude, gemini, copilot, grok. ⚠ google_ai_mode wird zwar validiert, aber im Fan-out STILL übersprungen (keine Response, keine Credits) — dafür TrackingProject-Pipeline nutzen |
1 pro Platform×Mode |
| GET | /prompt-check/{id} |
Detail: run + alle responses mit response_text, sentiment, brands_mentioned, citations, error_code (permanent_upstream|not_configured|null — bei gesetztem Code ist ein Retry zwecklos) |
— |
| POST | /prompt-check/{id}/rerun |
Re-Run gleicher Settings, neue run-id (202) | 1 pro Platform×Mode |
| GET | /prompt-check/{id}/export |
Download ?format=csv|json |
— |
| DELETE | /prompt-check/{id} |
Soft-delete | — |
Status-Lifecycle: pending → running → completed | partial | failed. Polling alle 3-5s.
Web-Search-Support: chatgpt, perplexity, claude, gemini. Copilot/Grok ignorieren das Flag. google_ai_mode ist IMMER grounded (Google AI Mode via DataForSEO) und läuft genau 1× pro Prompt — kein with/without-search-Doppel, kein Reality-Check-Blind-Pass.
Skills: prompt-check.{list-runs,get-run,create-run,export-run} — Chatbot-fähig via Agentic-Skill-Index.
| Method | URL | Notes |
|---|---|---|
| GET | /projects/{project}/articles |
paginated 20 |
| POST | /projects/{project}/articles |
Body: {title, slug?, content?, status?, cms_integration_id?, publish_status?(draft|scheduled), scheduled_publish_at?, cms_publish_options?}. cms_integration_id team-scoped (404 fremd); publish_status=scheduled braucht zukünftiges scheduled_publish_at (sonst 422). WordPress: cms_publish_options.rest_base (posts=Beitrag|pages=Seite, regex-whitelisted) + cms_publish_options.category_ids[] (int, nur für posts). Shopify: cms_publish_options.blog_id (int, Ziel-Blog) + cms_publish_options.tags (string, kommasepariert). Der Cron publiziert sobald fällig + Content vorhanden |
| GET | /articles/{id} |
full payload |
| PUT | /articles/{id} |
Body: {title?, slug?, content?, status?, meta_description?} + optional Publish-Felder {cms_integration_id?, publish_status?(draft|scheduled), scheduled_publish_at?, cms_publish_options?} zum nachträglichen Ändern (nur wirksam wenn cms_integration_id mitkommt; scheduled→422 ohne Zukunfts-Datum) |
| DELETE | /articles/{id} |
|
| GET | /articles/{}/optimization/runs |
Listet Score-Optimierungs-Runs eines Artikels paginiert |
| POST | /articles/{}/optimize-loop |
Startet agentische Score-Optimierungsschleife asynchron (202, Poll-URL) |
| POST | /articles/{}/submit-for-approval |
Artikel in einen Agentur-Freigabe-Workflow einreichen |
Scannt die Projekt-Domain nach vorhandenen Blog-Artikeln (Sitemap +
RSS-Autodiscovery + Blog-Pfad-Heuristik, JSON-LD datePublished/
dateModified wo vorhanden) und importiert eine ausgewählte Teilmenge als
Article (source='imported', imported_at, import_source_url).
Vorschau+Auswahl-Workflow — kein automatischer Voll-Import.
| Method | URL | Credits |
|---|---|---|
| POST | /projects/{project}/article-import/scan — Body optional {domain?} (Default: project.domain). Dispatched den Scan-Job, 202 mit {scan_id, status:"queued"} |
5 (charge-on-success, kein Treffer → keine Abbuchung) |
| GET | /projects/{project}/article-import/scans/{scan} — Status pollen (pending|running|completed|failed|no_results) + candidates_found |
— |
| GET | /projects/{project}/article-import/scans/{scan}/candidates — gefundene Kandidaten, paginiert (url, title, detected_published_at, detected_modified_at, status) |
— |
| POST | /projects/{project}/article-import/scans/{scan}/import — Body {candidate_ids:[]}. Erzeugt Article-Rows für die ausgewählten, noch nicht importierten Kandidaten. Returns {imported, skipped_already_imported} (bereits importierte werden übersprungen, kein Doppel-Import) |
0 (Kosten fielen beim Scan an) |
| Method | URL | Credits |
|---|---|---|
| POST | /articles/{id}/score |
— |
| POST | /articles/{id}/optimize |
— |
| POST | /articles/{id}/generate — Body: {keyword, article_type?, outline?, target_length?, tone?, language?, style_profile_id?, content_goal_id?, use_knowledge_base?, knowledge_mode?, knowledge_document_ids?}. knowledge_mode ∈ all|specific; bei specific werden knowledge_document_ids[] strikt auf die ready-Docs des Projekts gescopt (fremde IDs verworfen) |
5 |
| POST | /articles/{id}/publish — Body: {cms_integration_id}. Unterstützt WordPress und Shopify (canonical job-mapping). 202 mit {publication_id, status:pending}; 409 wenn bereits in flight; 422 bei nicht-unterstütztem CMS-Typ |
— |
| GET | /articles/{id}/publications — CMS-Veröffentlichungs-Historie des Artikels (neueste zuerst). data[]: {id, cms_integration_id, cms_name, cms_type, status, external_id, external_url, error_message, published_at, created_at}. status ∈ pending|publishing|published|failed |
— |
| GET | /articles/{id}/images — Artikelbilder (Beitragsbild zuerst) |
— |
| POST | /articles/{id}/images — KI-Bild generieren. Body: {prompt, model?, style?}. model ∈ dall-e-2/3, gpt-image-1/2, flux-schnell/dev, flux-1.1-pro. 202 mit image_id; 402 bei zu wenig Credits |
3–10 (je Modell) |
| POST | /articles/{id}/images/from-gallery — bestehendes Team-Galerie-Bild als Beitragsbild setzen. Body: {file_id} (AgenticUploadedFile-ID). Kopiert Datei auf public Disk, legt Image an, setzt exklusiv is_featured. 201 mit {image_id, url, is_featured}; 404 wenn File nicht im Team oder kein Bild (mime_type≠image/* — die Galerie zeigt nur Bilder, agent-generierte Dokumente wie xlsx/csv/md sind ausgeschlossen); 422 wenn Quelle weg |
— |
| PATCH | /images/{id}/featured — Bild als Beitragsbild markieren (exklusiv pro Artikel) |
— |
| POST | /articles/{id}/repurpose — Body optional {format:linkedin|twitter|instagram|newsletter|youtube_script|tiktok_script|facebook}. Ohne format → alle |
3 |
| Method | URL | Notes |
|---|---|---|
| GET | /articles/{id}/versions |
?limit=50 |
| POST | /articles/{id}/versions/{vid}/restore |
überschreibt article.content |
| Method | URL |
|---|---|
| GET | /articles/{id}/freshness |
| POST | /articles/{id}/freshness/check (read-only score, 0 credits — non-destructive) |
| POST | /articles/{id}/freshness/refresh (AI-rewrite → DRAFT + version backup, contentRefresh credits — never auto-publishes) |
| GET | /articles/{id}/freshness/history |
| Method | URL |
|---|---|
| GET | /articles/{id}/link-suggestions |
| POST | /projects/{project}/internal-links/analyze (async, 202 + run_id; 1 Credit/Artikel im Job — kein flat charge) |
| GET | /internal-links/runs/{run} (Run-Status/Progress pollen) |
| PUT | /link-suggestions/{id} (status=applied wendet den Link an + erreicht das Live-CMS; draft-applied wenn Quelle unveröffentlicht) |
| Method | URL | Credits |
|---|---|---|
| POST | /generate/article |
5 |
| POST | /generate/image |
5 |
| Method | URL | Credits |
|---|---|---|
| POST | /cms-integrations/{id}/test-connection — prüft die hinterlegten Credentials live (WordPress /users/me, Shopify /shop.json). Reiner Probe-Call (keine Persistenz, idempotent), Throttle 20/min. 200 mit {data:{ok:bool, detail}}; 404 fremdes Team; 422 nicht-unterstützter CMS-Typ |
— |
Phase-1-REST-Slice: Polling einer laufenden Bulk-Generierung (read-only, team-gescopt, 0 Credits). Idiom/Anti-Patterns: siehe
docs/modules/landingpage-generator.md→ „Workflow (GEO-Optimizer-Perspektive)".
| Method | URL |
|---|---|
| GET | /landingpages/{designSystem}/pages (?status=&page=&per_page= ≤100) — paginierte Seiten mit progress{state,percent,eta_seconds,elapsed_seconds,expected_seconds,started_at} + pro Seite cta{url,status,type,suggested_label,buttons} (buttons=Per-Button-Override oder null) + batch{total,done,in_flight,percent} + expected_seconds. Fremde/unbekannte designSystem → 404. |
| POST | /landingpages/{designSystem}/plan Body {keyword_ids[],keywords[]} (mind. eins) — Seiten (planned) aus bestehenden Keyword-IDs und/oder Freitext-Keywords planen → 201 {design_system_id,planned,skipped,capped}. keyword_ids auf das DS-Projekt re-skopt (fremde werden verworfen); 422 ohne gültige Keywords, 409 wenn DS nicht ready, 404 bei fremdem Team. 0 Credits (Generierung kostet separat). |
| PATCH | /landingpages/{designSystem}/cta Body {page_ids[],cta_url} — optionale CTA-Ziel-URL (1+ Seiten) zuweisen + async scrapen/KI-klassifizieren (202). Team-gescopt (ids auf DS+Team re-skopt), 0 Credits. |
| PATCH | /landingpages/pages/{landingPage}/cta-buttons Body {buttons:[{section,label,url}]|null} — Per-Button-CTA-Override EINER Seite (null=AUTO, []=keine, sonst genau diese, je url:http,https, ≤4). Synchron re-gerendert; Response {cta_buttons,effective[],rerendered}. Team-gescopt, fremde Seite → 404. 0 Credits. |
| Method | URL |
|---|---|
| GET | /rank-tracker |
| GET | /rank-tracker/{project} |
| GET | /rank-tracker/{project}/keywords (?device=&country=&is_active=) |
| POST | /rank-tracker/{project}/keywords Body: {keywords[], devices[]} async 202 |
| POST | /rank-tracker/{project}/refresh |
| GET | /rank-tracker/keywords/{keyword}/history (30d/90d/365d) |
| Method | URL | Credits |
|---|---|---|
| POST | /explorer Body: {seed, language?, country?, cache?}. cache=false umgeht 60-min-Cache |
50 |
| GET | /explorer/searches Such-Historie des Teams (paginiert, ?page=&per_page= max 100). Items: {id, keyword, language, country, created_at, searched_at} |
0 |
| POST | /explorer/searches/{id}/refresh Refresh eines History-Eintrags: frischer Pull (Cache-Bust), 202 + {keyword, language, country}. Abrechnung bei Job-Erfolg |
50 |
| DELETE | /explorer/searches/{id} History-Eintrag löschen (204; gecachte Ergebnisse bleiben). 404 bei fremdem Team |
0 |
| GET | /explorer/{keyword} Volume + KD + Trends + SERP. ?include=related,questions inlined die Listen. ?cache=false erzwingt fresh fetch |
0 / 50* |
| GET | /explorer/{keyword}/related ?type=related|question. ?cache=false |
0 / 50* |
| Method | URL |
|---|---|
| GET | /gap · /gap/{project} · /gap/{project}/opportunities |
| PATCH | /gap/opportunities/{opportunity} |
| POST | /gap/{project}/refresh |
| Method | URL |
|---|---|
| GET/POST | /site-audit |
| GET | /site-audit/{crawl} · .../issues |
| PATCH | /site-audit/{crawl}/issues/bulk |
| PATCH | /site-audit/issues/{issue} |
| POST | /site-audit/{crawl}/abort |
| POST | /site-audit/issues/{issue}/generate-brief |
| GET | /site-audit/{}/report.pdf |
| GET | /site-audit/{}/export |
Issues — response fields. Each item in GET /site-audit/{crawl}/issues returns:
{id, site_crawl_id, page_id, url, issue_type, severity, message, context, fix_priority, status, has_ai_brief, ai_brief_generated_at}. The url field is sourced from the crawl page (eager-loaded) and is null if the page row was deleted.
Grouped view — GET /site-audit/{crawl}/issues?group_by=type. Additive aggregation (mirrors the UI's grouped Probleme-tab): one row per (issue_type, severity) — {data:[{type, label, severity, total, open, fixed, dismissed}]}, sorted by severity rank (critical→notice) then total desc. label is the human-readable name (IssueTypeLabeler). Respects the severity and issue_type query filters (status is NOT pre-filtered — the counts expose the open/fixed/dismissed split). No pagination, no trend (trend is UI-only). group_by accepts only type (else 422). Without the param the per-issue shape above is unchanged.
Bulk-mark — PATCH /site-audit/{crawl}/issues/bulk. Throttle: 30/min. Body:
{
"filter": {
"issue_type": "missing_alt_text",
"severity": "high",
"status": "open"
},
"new_status": "fixed"
}
All filter.* fields are optional. filter.status is the FROM-state to match (only flip rows currently in this status). new_status is required and must be one of open|dismissed|fixed. Response: {data:{affected:int, new_status}, meta:{applied_filter:{...}}}.
Single-Mark — PATCH /site-audit/issues/{id}. Single-Issue-Status ändern. Body: {"status":"fixed"|"dismissed"|"open"}. Returns {id, status}. Idiom: NACH JEDEM angewendeten Fix MUSS der API-Caller diesen Endpoint mit status=fixed aufrufen — sonst bleiben Issues im Dashboard fälschlich als „open", und die „seit letztem Crawl behoben"-Vergleichsmetrik der nächsten Crawl-Iteration wird wertlos. Team-scope via Parent-Crawl.
Crawl-Summary Response — GET /site-audit/{crawl} returns {data: {id, status, pages_crawled, pages_failed, total_issues, crawl_depth, max_pages, credits_used, tracking_project_id, started_at, completed_at, bridge_dispatched_at, bridge_completed_at, created_at, error_code, error_message, error_text, deep_link}}. Fehlergrund seit Plan-Hub #92: error_code ist die maschinenlesbare Klasse (timeout, worker_restart, url_unreachable, host_invalid, sitemap_empty, aborted_by_user, unknown), error_message der gekappte Rohtext (max. 500 Zeichen), error_text die Fassung für Menschen — dieselbe Quelle, die die Detailseite zeigt. Alle drei sind null, solange der Lauf läuft oder sauber durchlief. Bridge-Timestamps seit 2026-05-14: bridge_dispatched_at zeigt „Grounding-Batch dispatched, Issues kommen noch", bridge_completed_at zeigt „alle grounding_*-Issues final". Caller-Polling-Condition: data.status='completed' && data.bridge_completed_at != null.
Abbruch — POST /site-audit/{crawl}/abort. Bricht einen Lauf im Status queued oder running ab; 0 Credits. Antwort {message, crawl_id, status:'aborted'}. 409 wenn der Lauf bereits beendet ist, 404 bei fremdem Team. Der Abbruch hält: der Crawler prüft den Status zwischen zwei Seiten und beendet sich, statt ihn danach mit completed zu überschreiben — anders als die beiden älteren Abbruch-Muster des Projekts, die nur ein DB-Flag setzen. Verbrauchte Credits werden nicht erstattet (Erstattung: Plan-Hub #93).
Auto-Grounding-Bridge. When a crawl completes, the system automatically dispatches an async Grounding-Audit batch (Grounding Page Standard v1.5, E-E-A-T, People-First) over up to the first 100 crawled pages. When that batch finishes, structured findings are imported as SiteCrawlIssue rows with issue_type prefix grounding_*. No extra credits charged — the bridge runs piggyback on the crawl. Idempotent: re-running the same crawl will not duplicate grounding_* issues (dedup by (site_crawl_id, issue_type, site_crawl_page_id)).
Format grounding_<framework>_<category>_<code>. Typical codes (15 detector rules in v1.5-gp framework):
grounding_v1_5_gp_h1_entity_only — H1 not pure entity namegrounding_v1_5_gp_lead_single_sentence_definition — lead is not a 1-sentence definitiongrounding_v1_5_gp_lead_retrieval_sentence — missing retrieval-stabilization sentencegrounding_v1_5_gp_h2_entity_prefix, grounding_v1_5_gp_disambiguation_blockgrounding_v1_5_gp_human_notice, grounding_v1_5_gp_timestamps_visiblegrounding_v1_5_gp_anchors_stable_ids, grounding_v1_5_gp_facts_dl_gridgrounding_v1_5_gp_jsonld_present, grounding_v1_5_gp_jsonld_faq_when_html_faqgrounding_v1_5_gp_head_canonical_absolute, grounding_v1_5_gp_eeat_author_bio_schemagrounding_v1_5_gp_faq_entity_repeated, grounding_v1_5_gp_volatile_dated_sourcedFilter via GET .../issues?issue_type=grounding_v1_5_gp_h1_entity_only. Bulk-mark via filter:{issue_type:"grounding_v1_5_gp_eeat_author_bio_schema"}.
| Method | URL | Beschreibung |
|---|---|---|
| GET | /backlinks · /backlinks/{project} · .../domains · .../anchors · .../events |
Phase 5 base |
| POST | /backlinks |
Activate-Endpoint: Backlink-Tracking für Projekt aktivieren (Subscription required) |
| POST | /backlinks/{project}/refresh |
Trigger Delta-Pull Job |
| POST | /backlinks/{project}/full-refresh |
Vollständigen DFS-Re-Pull triggern (Async 202) |
| GET | /backlinks/{project}/spam-scores |
Toxische Domains (Filter ?min_spam=60) |
| POST | /backlinks/{project}/spam-scores/refresh |
Queue EnrichSpamScoresJob (202) |
| GET | /backlinks/{project}/disavow-export |
text/plain Disavow.txt (Param ?threshold=80) |
| GET | /backlinks/{project}/timeseries |
Historische Velocity-Punkte (?granularity=daily|weekly|monthly) |
| POST | /backlinks/{project}/anchors/refresh |
Queue PullDetailedAnchorsJob (202) |
| GET | /backlinks/{project}/links |
TODO-050 Per-Link-Browser: einzelne Backlinks (source_url→target_url, anchor, follow). Filter ?sort=last_seen|first_seen|source_trust_flow|anchor_text, ?follow=dofollow|nofollow, ?lost=1. Credits 0 |
| GET | /backlinks/{project}/lost-links |
TODO-050 Verlorene/gelöschte Einzel-Links (is_deleted OR lost_at), sortiert nach lost_at desc. Credits 0 |
| GET | /backlinks/{project}/disavow-recommendations |
TODO-050 Top toxic_severity=high Referring-Domains + Gründe (Disavow-Empfehlung; vom Action-Center/Agent zitierbar). Credits 0 |
| POST | /backlinks/{project}/link-gap |
TODO-050 Competitor-Link-Gap starten (Async 202). Body {competitor_domains?:[]} (Default = TrackingProject competitor_domains). Credits 10 (backlink_link_gap) |
| GET | /backlinks/{project}/link-gap |
TODO-050 Letzten Link-Gap-Lauf lesen (processing_status → results[]). Credits 0 |
| POST | /backlinks/{project}/competitor-overlap |
Wettbewerber-Overlap starten (Async 202). Body {competitor_domains?:[] (Default = TrackingProject, max 12), min_matches?:1..5 (default 2)}. Reicher als Link-Gap: competitor_matches, links_me („+ich"), N×N competitor_pair_overlap-Matrix. Credits 15 (backlink_competitor_overlap) |
| GET | /backlinks/{project}/competitor-overlap |
Letzten Overlap-Lauf lesen — server-seitig gefiltert/sortiert/paginiert. Query ?only_gaps=1 (default) &min_matches=2&only_clean=1&min_tf=30&sort=matches|trust_flow&per_page=50&page=1. Kein Lauf → {data:null} (200, kein 404). Credits 0 |
Schneller Lookup für eine Domain ohne sie als Projekt anlegen zu müssen — z.B. Konkurrenz-Recherche, AI-Agent-Calls. {domain} darf Buchstaben/Ziffern/./- enthalten, alles andere → 404.
| Method | URL | Beschreibung |
|---|---|---|
| GET | /backlinks/by-domain/{domain} |
Summary (rank, total backlinks, referring domains, dofollow ratio, spam score) |
| POST | /backlinks/by-domain/{domain}/pull |
DFS-Pull triggern (Async 202) |
| POST | /backlinks/by-domain/{domain}/enrich-spam-scores |
Spam-Score-Enrichment für die Top-Domains (Async 202) |
| GET | /backlinks/by-domain/{domain}/domains |
Referring Domains (Filter: ?only_active, ?only_toxic, ?sort) |
| GET | /backlinks/by-domain/{domain}/anchors |
Anchor-Verteilung |
| GET | /backlinks/by-domain/{domain}/spam-scores |
Toxische Domains (Filter ?min_spam=60) |
| GET | /backlinks/by-domain/{domain}/disavow-export |
Google-Disavow.txt (?threshold=80) |
| GET | /backlinks/by-domain/{domain}/timeseries |
Velocity-History (?granularity=daily|weekly|monthly) |
| GET | /backlinks/by-domain/{domain}/links |
TODO-050 Per-Link-Browser by-domain (einzelne Backlinks; ?sort, ?follow, ?lost=1). Credits 0 |
| GET | /backlinks/by-domain/{domain}/lost-links |
TODO-050 Verlorene Einzel-Links by-domain. Credits 0 |
| GET | /backlinks/by-domain/{domain}/disavow-recommendations |
TODO-050 High-Toxicity Disavow-Empfehlungen by-domain. Credits 0 |
| POST | /backlinks/by-domain/{domain}/link-gap |
TODO-050 Competitor-Link-Gap by-domain starten (Async 202). Body {competitor_domains:[]}. Credits 10 (backlink_link_gap) |
| GET | /backlinks/by-domain/{domain}/link-gap |
TODO-050 Letzten Link-Gap-Lauf by-domain lesen. Credits 0 |
| POST | /backlinks/by-domain/{domain}/competitor-overlap |
Wettbewerber-Overlap by-domain starten (Async 202). Body {competitor_domains:[] (Pflicht), min_matches?:1..5 (default 2)}. Credits 15 (backlink_competitor_overlap) |
| GET | /backlinks/by-domain/{domain}/competitor-overlap |
Letzten Overlap-Lauf by-domain lesen (Query ?only_gaps=1&min_matches=2&only_clean=1&min_tf=30&sort=matches|trust_flow&per_page=50). Kein Lauf → {data:null}. Credits 0 |
| Method | URL | Beschreibung |
|---|---|---|
| GET | /teams/current/backlinks-subscription |
Aktive Subscription des Teams (Tier, Limits, Renewal) |
| POST | /admin/teams/{team}/backlinks-subscription |
Admin-Override: Subscription für anderes Team setzen (Body: {tier, limits?}) |
Ad-hoc-Datenanalyse: LLM-generierter Python-Code läuft in einer doppelt-gesicherten Sandbox (bubblewrap + systemd-run, kein Netz/FS/Env, 256 MB RAM, ~20 s Hard-Kill). Die Plattform exportiert die angeforderten Datasets team-korrekt als JSON nach data/<type>.json; das Script hat NIE DB-/Netz-/Env-Zugang. Nur Python-Stdlib. Letztes Mittel — wenn ein Endpoint die Frage beantwortet (insb. Link-Gap), diesen nutzen, nicht die Sandbox.
| Method | URL | Beschreibung |
|---|---|---|
| POST | /sandbox/runs |
Analyse-Run starten (Async 202 → {id, status:"pending"}). Body: {script, datasets?, session_id?}. Gate credits:5 (Balance-Check) + Flat-Deduktion 5 Credits am Trigger (kein Refund bei failed/timeout). Throttle 10/60 min. |
| GET | /sandbox/runs/{id} |
Run pollen (team-scoped, IDOR → 404). Response: {id, status, stdout, stderr, exit_code, duration_ms, error, created_at}. Credits 0 |
Request (POST /sandbox/runs):
{
"script": "import json; d=json.load(open('data/backlink_refdomains.json'))['rows']; print(sum(1 for r in d if r['toxic_severity']=='high'))",
"datasets": [ { "type": "backlink_refdomains", "domain": "example.com", "limit": 2000 } ],
"session_id": 123
}
Response (202): { "id": 42, "status": "pending" } — dann GET /sandbox/runs/42 bis status ∈ {completed, failed, timeout}; das Ergebnis steht in stdout.
Dataset-Katalog (je Run ≤ 5 Datasets, ≤ 10 MB/Dataset; jede Datei = Envelope {type, params, count, truncated, rows[]}):
| type | Parameter | rows enthalten |
|---|---|---|
backlink_summary |
domain |
0/1 Summary-Objekte (trust_flow, citation_flow, ref_domains, spam_score …) |
backlink_refdomains |
domain, limit? (≤5000, default 1000) |
Referring Domains: domain, trust_flow, citation_flow, backlinks, toxic_severity, is_lost |
link_gap |
domain |
Ergebnisse des neuesten completed Link-Gap-Laufs des Teams |
tracking_keywords |
project_id (muss dem Team gehören — fremd → 422) |
Keyword-Rows: keyword, keyword_group, search_volume, cpc, difficulty, search_intent |
Limits: Script ≤ 20 KB, stdout/stderr je ≤ 64 KB (Rest abgeschnitten + markiert). Stdlib-only: json, csv, statistics, collections, math, re, itertools, functools.
| Method | URL | Credits |
|---|---|---|
| GET | /projects/{project}/keywords |
— |
| POST | /projects/{project}/keywords |
— |
| POST | /projects/{project}/keywords/import |
10/100 KW bei cluster |
| POST | /projects/{project}/keywords/import/csv |
10/100 KW bei cluster |
| POST | /projects/{project}/keywords/ai-research |
5 |
| GET | /projects/{project}/keyword-researches |
— |
| GET | /keyword-researches/{id}/status |
— |
| GET | /keyword-researches/{id}/results.csv |
— |
| DELETE | /keywords/{id} |
— |
| POST | /keywords/research |
5 |
| POST | /keywords/{id}/expand |
10 |
| GET | /keywords/{id}/expansions |
— |
GET /projects/{project}/keywords liefert pro Keyword zusätzlich source
(deep|quick|import|ai_search — nur auf Recherche-Wurzeln; einzelne via POST angelegte Keywords bleiben ohne source) und parent_keyword_id;
Filter ?research_id={id} liefert nur die Keywords einer Recherche.
POST /projects/{project}/keywords/import — Keyword-Liste importieren (async, 202).
Body: {name?, rows: [{keyword, search_volume?, cpc?, difficulty?, intent?}], cluster?}
(max. 2000 Rows, Dedupe case-insensitive). cluster=true holt die Google-Top-10 je
Keyword und fasst Keywords mit ≥50 % SERP-Überschneidung zu Clustern zusammen —
Credits: 10 je angefangene 100 Keywords (402 bei zu wenig Guthaben).
Response: {research_id, keywords_received, cluster, credits_charged, status_url, results_csv_url}.
POST /projects/{project}/keywords/import/csv — CSV-Datei-Variante: multipart file
(csv/txt/xlsx/xls, max 1 MB / 2000 Rows), optional name, cluster. Spalten-Mapping
auto-erkannt über Header (de/en: keyword/suchbegriff/term, volumen/search_volume, cpc,
difficulty/schwierigkeit, intent), Zahlen locale-aware („1.200" → 1200).
Explizites Mapping: mapping[<feld>]=<spalten-index> (0-basiert; Felder: keyword,
search_volume, cpc, difficulty, intent) überschreibt die Auto-Erkennung pro Feld —
nicht angegebene Felder bleiben auto-erkannt. has_header=false für Dateien ohne
Header-Zeile (Zeile 1 zählt dann als Daten; mapping[keyword] ist Pflicht). Keine
Keyword-Spalte → 422 no_keyword_column + detected_headers. Response wie oben,
zusätzlich skipped + detected_mapping (= effektives Mapping). research_id ist
die Batch-ID.
POST /projects/{project}/keywords/ai-research — KI-Recherche mit
Unternehmenskontext starten (async, 202). Body: {limit?} (1–50, Standard 30).
Kein Saatbegriff — das ist der Unterschied zu POST /keywords/research: die
Begriffe leitet eine OpenAI-Websuche aus dem Projekt-Stammsatz ab (Branche, Marke,
Zielgruppen, Leistungen, Website). Response: {status: queued, research_id, source: "ai_search", credits, status_url}. Die KI liefert KEINE Suchvolumen — die Zahlen
holt anschliessend dieselbe DataForSEO-Messung wie beim Schnell-Pfad, die Werte sind
also gemessen, nicht geschaetzt. Je Vorschlag wird die Belegadresse gespeichert
(source_urls); ein leeres Array heisst „erhoben, aber ohne Beleg", null heisst
„nie erhoben". Fortschritt ueber status_url (derselbe Polling-Endpunkt wie beim
Import). 5 Credits, beim Start reserviert und bei einem Fehlschlag
zurückgebucht — ein Ausfall kostet also nichts, aber das Guthaben ist während
des Laufs gebunden. Reicht es nicht: 402 insufficient_credits. Läuft für
das Projekt bereits eine KI-Recherche, wird ein zweiter Start abgewiesen.
GET /keyword-researches/{id}/status — Batch-Polling: {status: queued|processing| clustering|completed|failed, is_finished, progress_percent, keywords_processed, keywords_pending, keywords_total, clusters_count, clustered_keywords, error_message, results_csv_url}. progress_percent (0–100, eine Nachkommastelle) = Anteil der
Keywords mit abgeschlossenem SERP-Fetch (der zeitfressende Teil); während des
finalen Clusterings gecappt auf 99, exakt 100 nur bei is_finished=true.
Backoff 30s empfohlen.
GET /keyword-researches/{id}/results.csv — Cluster-Ergebnis als maschinenlesbare CSV
(erst bei completed, sonst 409). Komma-separiert, UTF-8, snake_case-Header:
cluster_id,cluster_role,cluster_main_keyword,cluster_size,keyword,search_volume,cpc, difficulty,intent,article_titles — cluster_role ∈ main|sub|solo, jede Zeile trägt
cluster_id + cluster_main_keyword (selbsterklärend für LLM-Weiterverarbeitung).
Sortierung: größte Cluster zuerst, im Cluster main → subs nach Volumen.
GET /projects/{project}/keyword-researches — Recherche-Historie (Wurzeln mit
source, processing_status, keywords_count), paginiert.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /projects/{project}/storylines |
Liste aller Storylines (paginiert, 25/Seite) | 0 |
| POST | /projects/{project}/storylines |
TODO-047 async Outline-Generator: keyword (req), target_word_count?, goal_id?, language?, use_knowledge_base?/knowledge_mode?/knowledge_document_ids?[]. Erstellt generating-Platzhalter, dispatcht GenerateStorylineApiJob, liefert 202 {storyline_id,status:'generating'}. Job zieht die Credits einmal bei Erfolg. Legacy: Payload mit structure[] → synchroner Store (201, keine Generierung, keine Credits). |
5 |
| GET/PUT/DELETE | /storylines/{id} |
Detail (inkl. research_data — SERP heading-gap/PAA/featured snippet), Update (Status-Enum inkl. failed), Löschen |
0 |
| POST | /storylines/{id}/approve |
Review-Gate vor Artikel-Assembly. Setzt status=approved+approved_at+approved_by; idempotent |
0 |
| POST | /storylines/{id}/assemble-article |
Unified Storyline→Artikel (gleicher Pfad wie der In-App-Button): baut Article mit Projekt-Sprache + style_profile_id?/tone?, verknüpft articles.storyline_id+storyline.article_id, dispatcht GenerateArticleJob, liefert 202 {article_id}. 422 wenn nicht approved/completed. Single-use (2. Aufruf → bestehender Artikel) |
10 (= FeatureCost::article) |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /projects/{project}/style-profiles |
Auflisten (Response enthält zusätzlich source_type, source_url, pending_review, scraped_pages, failure_reason, reviewed_at) |
— |
| POST | /projects/{project}/style-profiles |
Manuell erstellen | — |
| GET | /style-profiles/{id} |
Detail (gleiche Zusatzfelder) | — |
| PUT | /style-profiles/{id} |
Aktualisieren. Wenn pending_review=true, wird das Flag implizit auf false gesetzt und reviewed_at=now() belegt (Detail-Edit zählt als Review-Akzeptanz). |
— |
| DELETE | /style-profiles/{id} |
Löschen | — |
| POST | /projects/{project}/style-profiles/from-url |
URL-basierte Stilanalyse als Background-Job starten. ScraperAPI lädt bis zu 10 inhaltsstarke Seiten, Claude erstellt das Profil. Fertiges Profil hat pending_review=true. HTTP 202 → { data: { id, status: "pending", source_url, message } } |
25 |
| GET | /style-profiles/{id}/scrape-status |
Polling-Endpoint für laufenden Background-Job. Liefert { status, source_url, scraped_pages, failure_reason, pending_review } |
— |
| POST | /style-profiles/{id}/accept |
Profil akzeptieren: setzt pending_review=false, reviewed_at=now(). Response: { data: <StyleProfile> } |
— |
| POST | /style-profiles/{id}/discard |
Profil verwerfen (Soft-Delete). Nur wenn pending_review=true, sonst 422. Response: { deleted: true } |
— |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /projects/{project}/knowledge-base |
Docs + Status auflisten (Polling) | — |
| POST | /projects/{project}/knowledge-base — Body: {title, content, type?} |
Rohtext ingestieren: persistiert, chunked + embedded async, gibt 202 {document_id, status:"processing"} zurück (kein 201/active mehr) |
5 |
| GET | /projects/{project}/knowledge-base/search — Query: {q, top_k?} |
Hybride Suche (Ollama-Embedding-Cosine ∪ Keyword, dann Haiku-Rerank) über ready-Chunks; {data:[{id,document_id,snippet,score}], meta} |
1 |
| DELETE | /knowledge-base/{id} |
Doc löschen (Cascade-Delete der Chunks) | — |
| GET | /knowledge-base/{id}/download |
Datei herunterladen (Original-Upload bzw. extrahiertes/synthetisiertes Markdown) | — |
| POST | /knowledge-base/url |
URL ingestieren (async) | 5 |
| POST | /projects/{project}/knowledge-base/site-analysis — Body: {url, max_pages?} (20–80, Default 40) |
Website-Analyse starten: agentische Pipeline crawlt die Domain, klassifiziert per LLM und synthetisiert bis zu 9 KB-Dokumente (source_type=site_analysis, Re-Analyse ersetzt alte Docs). 202 {analysis_id, status, poll_url}; 409 = Run für Host aktiv, 422 = SSRF |
25 |
| GET | /projects/{project}/knowledge-base/site-analysis |
Analyse-Runs eines Projekts (paginiert, mit progress_percent) |
— |
| GET | /knowledge-base/site-analysis/{id} |
Run-Status pollen: status (pending→discovering→extracting→synthesizing→completed|partial|failed), progress_percent, Seiten-Counter, primary_language, document_ids |
— |
| Method | URL |
|---|---|
| GET/POST | /projects/{project}/link-lists |
| DELETE | /link-lists/{id} |
| Method | URL |
|---|---|
| POST | /projects/{project}/orphan-scans/discover |
| POST | /orphan-scans/{id}/start |
| GET | /orphan-scans/{id} · .../pages |
| DELETE | /orphan-scans/{id} |
| Method | URL |
|---|---|
| GET | /content-intelligence |
| GET | /content-freshness |
| Method | URL |
|---|---|
| GET/POST | /projects/{project}/calendar |
| PUT/DELETE | /calendar/{id} |
| POST | /calendar/{entry}/generate-article |
| Method | URL | Beschreibung |
|---|---|---|
| GET | /projects/{project}/images |
Listet alle Bilder eines Projekts (legacy — die team-weite Gallery /v1/images unten ist der primäre Pfad) |
Cross-Team-IDs liefern 404 (nicht 403, kein Existenz-Leak).
| Method | URL | Credits | Beschreibung |
|---|---|---|---|
| GET | /images |
0 | Liste mit Filter: ?q, ?model, ?from, ?to, ?favorite, ?tags[], ?sort=newest|oldest|edited|name|model|size|date, ?dir=asc|desc (nur für name/model/size/date — spiegelt die Spalten-Sortierung der Galerie-Tabellenansicht), ?page, ?per_page |
| GET | /images/trash |
0 | Soft-deleted Bilder |
| GET | /images/{id} |
0 | Detail inkl. variants[] und parent-Tree |
| POST | /images/{id}/edit |
10 | Edit-Variante via OpenAI Images-Edit-API (openai:/images/edits, extern). Async 202, Body: {job_id, edit_session_id}. Frontend pollt /images/{id} |
| DELETE | /images/{id} |
0 | Soft-delete (30 Tage Trash, dann hard-delete) |
| POST | /images/{id}/restore |
0 | Wiederherstellen aus Trash |
| DELETE | /images/{id}/forever |
0 | Endgültig löschen (NUR aus Trash, sonst 409) |
| POST | /images/{id}/favorite |
0 | Toggle is_favorite |
| PATCH | /images/{id} |
0 | Update Metadaten: {title?, tags?[]} |
| POST | /images/bulk/delete |
0 | Body {ids:[]} max 200 |
| POST | /images/bulk/restore |
0 | Body {ids:[]} max 200 |
| POST | /images/bulk/export |
0 | Body {ids:[]} max 500. Async 202 → {job_id} für ZIP |
| GET | /images/exports/{job_id} |
0 | Export-Status: {status: queued|ready|failed, download_url?} |
| POST | /images/{id}/open-in-chat |
0 | Erstellt AiChatSession mit gallery_image_id={id}. Returns {session_id} für Redirect zu /chat/{session_id} |
| POST | /images/{id}/share |
0 | Idempotenter Public-Share-Token. Body: {expires_at?}. Returns {token, public_url, expires_at, view_count, is_active} |
| PATCH | /images/{id}/share |
0 | Expiry ändern. Body: {expires_at: ISO|null} (404 wenn kein aktiver Share) |
| DELETE | /images/{id}/share |
0 | Public-Share widerrufen (View-Count bleibt erhalten) |
Komplement zur Image Gallery: alles mit mime_type ≠ image/* (agent-generierte xlsx/csv/md + User-Uploads). Cross-Team- ODER Bild-id liefert 404. Liste zeigt nur lebende Dateien (expires_at IS NULL OR >= now). Items: {id, original_name, type_label, size_bytes, mime_type, session{id,title}, expires_at, expires_in_days, pinned, is_favorite, tags, download_url}.
| Method | URL | Credits | Beschreibung |
|---|---|---|---|
| GET | /files |
0 | Liste mit Filter: ?q (Name), ?type (mime-Teilstring), ?session_id, ?expiring=1, ?sort=newest|oldest|name|size, ?page, ?per_page (≤100) |
| GET | /files/trash |
0 | Soft-deleted Dateien des Teams |
| GET | /files/{id} |
0 | Detail inkl. session + download_url |
| POST | /files/{id}/keep |
0 | Macht die Datei dauerhaft (pinned_at, expires_at=NULL) — gegen 30-Tage-Auto-Ablauf |
| POST | /files/{id}/favorite |
0 | Toggle is_favorite |
| PATCH | /files/{id} |
0 | Update Metadaten: {title?, tags?[]} |
| DELETE | /files/{id} |
0 | Soft-delete (Papierkorb) |
| POST | /files/{id}/restore |
0 | Wiederherstellen aus Papierkorb |
| DELETE | /files/{id}/forever |
0 | Endgültig löschen (NUR aus Papierkorb, sonst 409) |
| POST | /files/bulk/delete |
0 | Body {ids:[]} max 200 |
| POST | /files/bulk/restore |
0 | Body {ids:[]} max 200 |
| POST | /files/bulk/export |
0 | Body {ids:[]} max 500. Async 202 → {job_id} für ZIP |
| GET | /files/exports/{job_id} |
0 | Export-Status: {status, download_url?} |
| POST | /files/{id}/share |
0 | Idempotenter Public-Share-Token. Returns {token, public_url, expires_at, view_count, is_active}. public_url = /share/file/{token} |
| PATCH | /files/{id}/share |
0 | Expiry ändern (404 wenn kein aktiver Share) |
| DELETE | /files/{id}/share |
0 | Public-Share widerrufen |
| Method | URL | Credits |
|---|---|---|
| POST | /agentic/chat |
varies (depends on tools used by the agent) |
Body:
message (string, required, max 10k): User-Anfrage in natürlicher Sprachesession_id (int, optional): bestehende Session fortführenQuery-Parameter:
wait_for_final (bool, default true): bei wait_and_recheck-Handoff (z.B. asynchroner DataForSEO-Pull) pollt der Endpoint serverseitig bis zur finalen Resume-Antwort (max ~480 s). Auf false setzen um sofort die Placeholder-Nachricht zu erhalten.Response 200: {session_id, message_id, text, tool_calls, iterations, credits_used, duration_ms, resumed}
resumed: true heißt: der Master-Agent hat mid-turn wait_and_recheck getriggert und der Endpoint hat erfolgreich auf den Resume-Turn gewartet. Der text ist dann die finale Antwort, nicht der Wartehinweis.
Hinweise:
rankion-agent| Method | URL | Credits | Notes |
|---|---|---|---|
| POST | /ai-scanner/detect |
2 | scan_type (alias mode): quick = sync 200 (heuristic) · deep = async 202 {scan_id,status} → poll status. language (de/en/es/fr) persisted. Every scan stored in /ai-scanner history. Failed deep scan auto-refunds. |
| GET | /ai-scanner/scans/{id} |
— | Poll an AI-detection scan: {scan_id,scan_type,processing_status,score,details,word_count,language,credits_charged}. Team-scoped. |
| POST | /ai-scanner/humanize |
dyn | Humanize an existing article_id (async 202). Cost ceil(words/1000) × {light:2,standard:4,deep:8} — job-deducted, controller pre-flight gates. |
| Method | URL | Credits |
|---|---|---|
| POST | /humanize |
8 |
| GET | /humanize/{batch} · .../documents/{document} |
— |
| POST | /humanize/{}/documents/{}/rehumanize |
Re-humanize an existing document (queue another LLM rewrite, async) |
| GET | /humanize/{}/documents/{}/rescore |
Re-score a humanize document's human-score after manual edits (free) |
| Method | URL | Credits |
|---|---|---|
| GET | /content-optimizer · /content-optimizer/{id} · /content-optimizer/optimizations/{id} (alias) |
— |
| POST | /content-optimizer/analyze |
5 |
| POST | /content-optimizer/{id}/apply · /content-optimizer/optimizations/{id}/apply (alias) |
5 |
| DELETE | /content-optimizer/{id} (Soft-Delete, 204) |
— |
Async-Vertrag (Subsystem B P0.3 — fixed 2026-05-09):
POST /v1/content-optimizer/analyze returns 202 mit:
{
"message": "Content optimization started",
"id": 6,
"optimization_id": 6,
"poll_url": "/api/v1/content-optimizer/6"
}
Plus Header Location: /api/v1/content-optimizer/6.
Polling-Pfad: GET /api/v1/content-optimizer/6 (canonical) — der Pfad /optimizations/6 funktioniert auch (Alias-Route, weil das Field heisst optimization_id). Caller können beides.
optimization_id bleibt für Kompatibilität, aber neue Caller sollten id lesen.
| Method | URL | Credits |
|---|---|---|
| GET/POST | /competitor-analyses |
20 (POST) |
| GET | /competitor-analyses/{id} · .../gaps (?is_favorite=&is_dismissed=) — competitors[] je Wettbewerber mit Majestic-Autorität trust_flow / citation_flow / ref_domains / authority_checked_at (null bis angereichert) |
— |
| PATCH | /competitor-analyses/{id} Body {name} (umbenennen) |
— |
| DELETE | /competitor-analyses/{id} (inkl. Gaps, 204) |
— |
| PATCH | /competitor-analyses/gaps/{id} Body {is_favorite?, is_dismissed?} |
— |
| POST | /competitor-analyses/{id}/rerank-competitors |
— |
Site-wide content-quality crawler. Walks a domain (sitemap-discovered), scores each page on SEO (0–100), content quality (0–100), word count, and emits issues[] = {type:error|warning|info, category, message} plus recommendations[]. Distinct from /site-audit — this module is the content-quality angle, /site-audit is the crawl/issue/grounding angle.
| Method | URL | Credits |
|---|---|---|
| GET/POST | /content-audits |
10 (POST) |
| GET | /content-audits/{id} · .../export · .../pages |
— |
| GET | /content-audits/{id}/compare |
— |
| PATCH | /content-audits/{id}/pages/bulk |
— |
| GET | /content-audit-pages/{id} |
— |
| PATCH | /content-audit-pages/{id}/solved |
— |
| POST | /content-audit-pages/{id}/refresh |
1 |
Audits-listing — filters on GET /content-audits. Query params:
tracking_project_id=<int> — filtert auf Audits eines Tracking-Projects (FK→tracking_projects.id).source_url=<string> — exact-match auf die Audit-URL (Audit ist 1:1 mit URL gescoped).status=running|completed|failedtype=full|quick (UI-Pfade sitemap/single werden nicht über die API gestartet).project_id → 422 (Variante 1 Rename 2026-05-10) — Migration-Hint im Response. tracking_project_id ist die kanonische FK, parallel zum /grounding-Pattern.Listing-Schema (data[].keys): id, tracking_project_id, source_url, type, status, total_pages, avg_seo_score, total_issues, created_at. Detail-Schema (/content-audits/{id}) enthält zusätzlich team_id, user_id, project_id (legacy), tracking_project_id, summary, ….
POST-Body akzeptiert: {source_url, type?:full|quick, tracking_project_id?}. project_id → 422 mit Migration-Hint. tracking_project_id wird via exists:tracking_projects,id validiert und persistiert.
Pages-listing — filters on GET /content-audits/{id}/pages. Query params:
status=open|solved — open is the canonical alias for is_solved=false. Anything other than solved resolves to open.priority=critical|high|medium|lowmin_seo_score=0..100, max_seo_score=0..100issue_type=<string> — matches any issues[].type value (canonical severity buckets are error, warning, info). Implemented as JSON-LIKE on the serialized issues column.sort= one of url|title|seo_score|content_quality_score|word_count|priority|is_solved, dir=asc|desc, per_page= (default 50).Response shape: {progress:{total, solved, open, percent}, pages:{data:[…], current_page, last_page, …}}.
Audit-over-time compare — GET /content-audits/{id}/compare. Read-only, 0 credits, runs no new crawl (pure read of the persisted summary + page issues). Compares {id} (current) against a baseline:
against=<int> — explicit baseline audit id (team-scoped; 422 if not found / foreign team).source_url.baseline: null and null trend deltas (never 404/500).Response shape: {current, baseline, trend:{avg_seo_score_delta, avg_quality_score_delta, total_issues_delta, by_type_delta:{error,warning,info}}, issues:{new:[…], fixed:[…], persistent:[…], by_type_delta}}. Deltas = current − baseline. Issue identity is normalized (url + type + message-core, numbers/brackets stripped) so a re-measured count is not flagged as "new".
Single-page refresh — POST /content-audit-pages/{id}/refresh. 1 credit (credits-auto:1, SSOT config/credits.php flat content_audit_refresh_page). Async re-crawl of one page: sets row status=pending, dispatches RefreshAuditPageJob, returns 202 {page_id, status:queued}. Guarded on the parent audit being completed (else 422) so a mid-flight batch crawl can't re-grab the page. Throttle: 30/min. Poll GET /content-audit-pages/{id} for the pending→processed transition.
Bulk-mark — PATCH /content-audits/{id}/pages/bulk. Throttle: 30/min. Body:
{
"filter": {
"priority": "low",
"status": "open",
"min_seo_score": 0,
"max_seo_score": 50,
"issue_type": "error"
},
"is_solved": true
}
All filter.* fields are optional. filter.status is the FROM-state to match (only flips rows currently in this state). is_solved is required and is the new state to write. Response: {data:{affected:int, is_solved:bool}, meta:{applied_filter:{…}}}. Team-scoped via audit.team_id — cross-team access returns 403.
filter.project_id → 422 (Variante 1 Rename 2026-05-10): Pages haben keinen Project-FK; das Projekt ergibt sich aus dem Parent-Audit. Vorher wurde filter.project_id stumm gedroppt — was Mass-Updates über alle Pages des Audits zog statt nur über die eines Projekts. Jetzt 422 mit klarem Hint.
Nur für Administratoren. Die Kette ist auth:sanctum plus admin — ein
gewöhnlicher Kunden-Token bekommt 403. Grund: ein Community-Mitglied gehört
keinem Team an, es gibt also nichts zu scopen; ohne das Admin-Gate bekäme jeder
Token Klarnamen und Mailadressen aller Mitglieder. Dieselbe Bauart wie
/v1/outbound/*.
| Method | URL | Credits |
|---|---|---|
| GET | /community/members — Filter: q, membership_level (std|pro), status (aktiv|gesperrt|unbestaetigt), per_page (max 100), page. Antwort: {data:[…], meta:{current_page, per_page, total, last_page}} |
— |
| GET | /community/members/{member} — ein Mitglied. 404 bei unbekannter Nummer. |
— |
| GET | /community/members/{member}/membership — die Mitgliedschaft eines Mitglieds: Stufe, Status, Laufzeitende, Abo-Kennung. is_running beantwortet die eigentliche Frage »läuft sie?« und schließt active, trialing und past_due ein. Nur lesend — eine Mitgliedschaft entsteht durch eine Zahlung; sie hier zu setzen ließe Katalog und Stripe auseinanderlaufen. |
— |
Kennwort und remember_token verlassen die Schnittstelle nie (im Model als
$hidden, zusätzlich durch eine ausdrückliche Feldliste im Controller).
Die Mitgliederzeile trägt seit Plan-Hub #115 zusätzlich membership
(plan_id, plan_label, status, ends_at, stripe_subscription_id) und
team_id. Die Feldliste im Controller ist eine ausdrückliche Aufzählung —
ein neues Feld am Model erscheint dort nicht von selbst.
Die dritte Routengruppe dieses Bereichs und die einzige ohne Riegel
(Plan-Hub #115, Position 14). Das ist kein Versehen: die Stufen und ihre Preise
stehen ohnehin auf der öffentlichen Kaufseite unter /community/mitgliedschaft.
Sie hinter einem Token zu verstecken schützte nichts und machte den Katalog für
ein Fremdsystem unbrauchbar, das einen Preis anzeigen will. Gedrosselt auf
60 Anfragen je Minute — offen heißt nicht unbegrenzt.
| Method | URL | Credits |
|---|---|---|
| GET | /community/plans — der Stufen-Katalog. Ohne Token lesbar. include_inactive=1 nimmt abgeschaltete Stufen mit (Vorgabe: nur aktive). Felder: id, slug, label, price_cents, price_eur (Zeichenkette mit zwei Nachkommastellen), currency, included_rankion_plan, is_default, is_active, sort_order. |
— |
| GET | /community/plans/{plan} — eine Stufe. 404 bei unbekannter Nummer. |
— |
Die Stripe-Kennungen (stripe_product_id, stripe_price_id) verlassen diese
Endpunkte nicht: sie sind keine Geheimnisse, gehören aber nicht in eine
Preisliste. price_eur ist bewusst eine Zeichenkette — als Zahl serialisiert
macht JSON aus 35.00 die 35 und aus 35.50 die 35.5, und ein Feld, dessen
Typ zwischen ganzzahlig und gebrochen wechselt, ist für ein Fremdsystem eine
Fehlerquelle. Die tragende Wahrheit ist ohnehin price_cents.
Kanonisch seit 03.09.2026 (Plan-Hub #114): /v1/community-monitor/*. Der
Wortstamm community gehört seither der öffentlichen Community mit eigener
Mitgliederverwaltung. Die alten Adressen bleiben als Zweitname bestehen und
antworten unverändert — kein Fremdsystem muss umgestellt werden.
| Method | URL | Credits |
|---|---|---|
| GET | /community-monitor/mentions · /community-monitor/mentions/{id} |
— |
| POST | /community-monitor/scan Body: {keyword, platforms[]} |
5 |
| GET | /community-monitor/alerts |
— |
| GET | /community/mentions · /community/mentions/{id} — Zweitname, unverändert |
— |
| POST | /community/scan — Zweitname, unverändert |
5 |
| GET | /community/alerts — Zweitname, unverändert |
— |
| Method | URL |
|---|---|
| GET/POST | /site-monitor |
| GET/PUT/DELETE | /site-monitor/{id} |
| POST | /site-monitor/{id}/pause · .../resume · .../check-now |
| GET | /site-monitor/{id}/checks · .../incidents · .../lighthouse |
| Method | URL | Credits |
|---|---|---|
| GET/POST | /bulk-generations Body: {project_id, keywords[], type?, length?} |
15 (POST) |
| GET | /bulk-generations/{id} |
— |
| Method | URL |
|---|---|
| GET/POST | /autopilot Body: {module, frequency, config} |
| PUT/DELETE | /autopilot/{id} |
| GET | /autopilot/{id}/runs real per-run history (keyword, status, geo_score, skip_reason, article link). ?status= filter. Read-only, 0 credits |
Auto-publish quality gate (TODO-059): when settings.auto_publish=true, each run scores the generated content with the deterministic GEOScorerService and only publishes (and pushes to a connected CMS) if total_score >= settings.min_geo_score (default config('autopilot.min_geo_score')=70). Below threshold → kept as a draft + a skipped_low_score run row; never published. Each run records exactly one row (success/skipped_no_credits/skipped_no_keyword/skipped_low_score/failed). Credit per article = FeatureCost::article(word_count); the gate scoring itself is free.
| Method | URL | Credits |
|---|---|---|
| GET/POST | /pipelines Body: {name, stages[]} |
20 (POST) |
| GET | /pipelines/{id} |
— |
| POST | /pipelines/{id}/retry |
— |
| Method | URL |
|---|---|
| GET/POST | /projects |
| GET/PUT/DELETE | /projects/{id} |
| GET/POST | /goals · DELETE /goals/{id} |
| GET | /modules |
| POST | /modules/{module}/activate · .../deactivate |
POST /projects Body: {name, domain?, language?, description?, country?, additional_countries[]?}
— country ist der einzelne Primärwert (projects.country, unverändert). additional_countries[]
(max 10, je ISO-3166-1 alpha-2) legt/nutzt pro Land ein setup-Tracking-Gerüst
(tracking_projects+tracking_keywords-Platzhalterzeile) an, siehe docs/modules/tracking-projects.md.
Antwort trägt dann zusätzlich additional_markets: {registered[], skipped[], reason} — reason
ist no_domain (Projekt ohne Domain) oder active_tracking_project (Domain hat bereits aktives
Tracking, kein Platzhalter wird angelegt); das Feld fehlt ganz, wenn keine additional_countries
gesendet wurden.
| Method | URL |
|---|---|
| GET/POST/PATCH | /action-center/goal-profile |
| GET | /action-center/goal-templates · .../industry-templates · .../seo-detectors |
| GET | /action-center/path · /action-center/paths |
| POST | /action-center/path/recompute |
| GET/POST | /action-center/todos |
| GET/DELETE | /action-center/todos/{id} |
| GET | /action-center/todos/{id}/outcome — measured outcome (baseline vs realized, 14d delta) · 0 credits |
| POST | /action-center/todos/{id}/complete · .../snooze |
| POST | /action-center/todos/{}/move |
| POST | /action-center/todos/{}/notes |
| Method | URL | Credits |
|---|---|---|
| GET | /google/connections |
— |
| DELETE | /google/connections/{id} |
— |
| GET | /google/gsc/properties |
— |
| POST/DELETE | /google/gsc/properties/{p}/link |
— |
| POST | /google/gsc/properties/{p}/sync |
1 |
| GET | /google/gsc/properties/{p}/metrics |
— |
| GET | /google/ga4/properties |
— |
| POST/DELETE | /google/ga4/properties/{p}/link |
— |
| GET | /google/ga4/properties/{p}/metrics |
— |
| GET | /integrations/google/reviews/status |
— |
| Method | URL |
|---|---|
| GET | /review-sources · /review-sources/{id} |
| DELETE | /review-sources/{id} |
| POST | /review-sources/{id}/rotate-key · .../disconnect · .../reconnect |
| POST | /review-sources/webhook · /review-sources/waitlist |
| POST | /webhooks/reviews/{source} (public, key-auth) |
| Method | URL |
|---|---|
| GET/POST | /blog/categories |
| PUT/DELETE | /blog/categories/{id} |
| GET/POST | /blog/posts |
| GET/PUT/DELETE | /blog/posts/{id} |
| Method | URL |
|---|---|
| PUT | /os/mode |
| GET/POST | /os/preferences |
| GET/POST | /os/files |
| GET/PUT/DELETE | /os/files/{id} |
| POST | /os/files/{id}/move · .../share |
| DELETE | /os/files/{id}/share |
| GET/POST | /os/folders |
| GET/PUT/DELETE | /os/folders/{id} |
| POST | /os/folders/{id}/move |
| GET | /os/desktop · /os/spotlight |
| Method | URL |
|---|---|
| GET | /team |
| GET | /credits · /credits/history |
| GET | /dashboard |
| GET | /admin/api-costs (Admin only) |
| GET | /admin/usage-analytics |
| GET | /admin/online-users (Admin only) — wer ist gerade angemeldet: Name, E-Mail, letzte Aktivitaet je Konto, zuletzt aktiv zuerst, je Konto GENAU EINE Zeile (Plan-Hub #70). Dieselbe Quelle und derselbe 60-s-Zwischenspeicher wie die Zahl auf /admin. Optional window_minutes (1–60, Vorgabe 5). Liefert {data:[{id,name,email,last_activity_at,last_active_seconds_ago}], meta:{total,window_minutes,generated_at}}. Nicht paginiert — die Menge ist durch das Zeitfenster begrenzt. Keine IP-Adresse, kein User-Agent (Datensparsamkeit). |
| GET | /admin/promo-codes (Admin only) — Liste aller Promo-Codes mit Status (aktiv/abgelaufen/ausgeschöpft/deaktiviert) + fertiger register_url ({app-url}/register?promo=CODE) |
| GET | /admin/promo-codes/redemptions (Admin only) — Einlösungen, filterbar per year/month/day/per_page |
| GET | /admin/modules (Admin only) — alle Module + plattformweiter Admin-Sperrzustand (Plan-Hub #41, siehe module-store.md) |
| POST | /admin/modules/{module}/state (Admin only) — Sperrzustand setzen (state: active/disabled/hidden; 422 Kernmodul/unbekannt) |
| POST | /admin/invoices/{invoice}/cancel (Admin only) — Rechnung ueber die Lexware-API stornieren (Plan-Hub #62). Body {reason} (Pflicht, 3–500 Zeichen) — geht in den Belegtext der Stornorechnung und ins Admin-Protokoll. Erzeugt eine mit der Rechnung VERKNUEPFTE, finalisierte Gutschrift (POST /v1/credit-notes?finalize=true&precedingSalesVoucherId=…); die Public API hat keinen Void-Endpoint. Reine Buchhaltung: Credits, Plan und Abo bleiben unangetastet. Idempotent — ein zweiter Aufruf erzeugt keinen zweiten Beleg. 422 not_cancellable (bezahlt, bereits storniert oder ohne Lexware-Verknuepfung), 502 lexware_error (Lexware lehnt ab, z. B. Beleg nicht open — es entsteht dann KEIN Beleg). Antwort enthaelt credit_note_number und credit_note_pdf_url. Throttle 20/min. |
Team-Rechnungsprofil (Firmenanschrift, USt-IdNr., Rechnungsempfänger, abweichende Anschrift, PO-Referenz). Wird beim Schreiben nach Stripe gesynct.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /billing/profile |
Rechnungsprofil lesen. Leeres Shape, wenn noch keins existiert | — |
| PUT | /billing/profile |
Rechnungsprofil schreiben (validiert; USt-ID-Format je EU-Land; triggert Stripe-Customer-Sync). Gate: manage settings (Owner/Admin). Body: {company_name, address_line1, postal_code, city, country, contact_name?, address_line2?, vat_id?, billing_email?, purchase_order_ref?, separate_billing_address?, billing_address_line1?, billing_postal_code?, billing_city?, billing_country?} |
— |
| POST | /billing/invoice-upgrade |
Upgrade auf einen bezahlten Plan auf Rechnung (ohne Stripe): Plan sofort aktiv, offene PDF-Rechnung + Info-Mail an den Betreiber. Voraussetzung: vollständiges Rechnungsprofil (sonst 422) + hinterlegte Bankverbindung. Gate: manage settings. Body: {plan: starter|pro|agency, cycle: monthly|yearly} → 201 {invoice_id, invoice_number, plan, billing_cycle, amount, currency, status, due_date} |
— |
| POST | /billing/subscription/cancel |
Abo zum Ende der bezahlten Periode kündigen. Bewusst kein Sofort-Storno: die laufende Periode ist bezahlt und bleibt nutzbar, es wird nichts erstattet. Nur die Eigentümerin/der Eigentümer des Teams (sonst 403); ohne aktives Abo 404. → 200 {status: cancelled_at_period_end, plan, billing_cycle, access_until}. Throttle 5/min |
— |
Use-Case: Vor dem ersten Checkout das Rechnungsprofil setzen, damit Stripe-Rechnungen korrekte Firmenanschrift + USt-IdNr. (Reverse-Charge) tragen.
Idiom: GET /billing/profile (Ist-Stand lesen) → PUT /billing/profile (vollständiges Profil schreiben, 422 bei ungültigem USt-ID-Format/fehlenden Pflichtfeldern) → der PUT synct synchron nach Stripe; vat_id_status (Stripe-Echo) erscheint beim nächsten GET. Single-Resource (kein Bulk, kein Polling).
Anti-Patterns: Kein wiederholtes PUT in Schleife (jeder PUT triggert einen Stripe-Sync). USt-ID nicht selbst „verifizieren" — das macht Stripe; nur den vat_id_status lesen.
Cross-Module: /billing (Checkout/Invoices) liest dasselbe Profil; InvoiceMail geht an billing_email ?? owner. Modul-Doc: docs/modules/billing.md.
Mitglieder-Verwaltung + Einladungen eines Teams. Alle Endpoints team-scoped auf currentTeam() des Token-Users; Gate manage members (Owner/Admin). Delegiert an TeamMembershipService (Single-Source der Mutationen). Fremdes-Team-Member/-Einladung → 404; Service-Regelverletzung (z. B. Owner ändern, sich selbst entfernen, Plan-Rolle, Sitzplatz-Limit) → 422 mit message.
| Method | URL | Beschreibung | Gate | Credits |
|---|---|---|---|---|
| GET | /team/members |
Mitglieder-Liste {id, name, email, role, is_owner, last_active_at}. Owner wird immer als owner ausgewiesen. |
manage members | — |
| PATCH | /team/members/{user} |
Rolle ändern. Body {role}. admin darf nur der Owner vergeben/entziehen; Owner-Rolle nicht änderbar; eigene Rolle nicht änderbar. 404 fremdes Team, 422 Plan-Rolle/Regelverstoß. |
manage members | — |
| DELETE | /team/members/{user} |
Mitglied entfernen (+ zugehörige project_user-Einträge; current_team_id des Entfernten wird genullt, wenn es dieses Team war). Owner/Selbst nicht entfernbar (422). |
manage members | — |
| GET | /team/invitations |
Offene (nicht angenommene, nicht abgelaufene) Einladungen {id, email, role, expires_at}. |
manage members | — |
| POST | /team/invitations |
Einladen. Body {email, role}. 422 bei Sitzplatz-Limit oder im Plan nicht verfügbarer Rolle. Versendet Einladungs-Mail (7 Tage gültig). 201. |
manage members | — |
| DELETE | /team/invitations/{id} |
Einladung widerrufen. Bereits angenommene → 422. 404 fremdes Team. |
manage members | — |
Use-Case: Team-Sitzplätze verwalten — Kollegen mit passender Rolle einladen, Rollen anpassen, ausgeschiedene Mitglieder entfernen.
Idiom: GET /team/members + GET /team/invitations (Ist-Stand) → POST /team/invitations {email, role} (einladen; 422 bei Limit/Plan-Rolle) → nach Annahme erscheint die Person in /team/members, die Einladung verschwindet aus /team/invitations → PATCH /team/members/{user} für spätere Rollen-Änderungen, DELETE zum Entfernen. Single-Resource, kein Bulk, kein Polling (Annahme passiert async durch den Eingeladenen via Accept-Link).
Anti-Patterns: Keine Owner-Rolle per PATCH/POST vergeben (nur via Ownership-Transfer). Nicht wiederholt dieselbe E-Mail einladen — offene Einladungen sind idempotent (dieselbe Einladung wird wiederverwendet, kein Duplikat). Rolle nicht aus einem hardcoded Set wählen — nur Rollen aus RoleOptions::forTeam() (plan-abhängig) sind gültig, sonst 422.
Cross-Module: Rollen/Permissions stammen aus TeamRole + HasTeamPermissions::canDo(); UI-Pendant ist die Team-Settings-Surface. Modul-Doc: docs/modules/team-members.md.
Auditiert eine einzelne Landingpage: 3 Source-Screenshots (Desktop/Tablet/Mobile mit echter UA-Emulation), Lighthouse-Metriken, Opus 4.7 Multimodal-Vision-Analyse plus bis zu 3 gpt-image-2 KI-Renders der optimierten Variante (Desktop ist die Design-Basis, Tablet+Mobile sind responsive Adaptionen). Authentifizierung: Authorization: Bearer <token>, alle Endpoints team-scoped.
| Method | URL | Credits | Beschreibung |
|---|---|---|---|
| POST | /page-audit |
30 + 15 | Audit starten. Body: {url, tracking_project_id?, persona?} → 202 + {id, status:"pending", url} |
| GET | /page-audit/{id} |
— | Detail mit 2 Bild-URLs (Desktop-Source + Desktop-Ideal) + opus_analysis + Lighthouse |
| GET | /page-audits |
— | Paginierte Liste. ?per_page=25&tracking_project_id= |
Pipeline-Status: pending → scraping → screenshotting → analyzing → completed. Der Desktop-KI-Render läuft in einem separaten Background-Job nach completed; ideal_screenshot_url füllt sich anschließend.
Polling-Pattern:
# 1) Audit starten
ID=$(curl -s -X POST $BASE/page-audit \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"url":"https://example.com/landing"}' | jq -r '.id')
# 2) Pollen bis completed + Desktop-Ideal-Render fertig (Hauptflow ~1-2 min, Desktop-Render +2-3 min)
while true; do
R=$(curl -s -H "Authorization: Bearer $TOKEN" $BASE/page-audit/$ID)
STATUS=$(echo "$R" | jq -r '.status')
IDEAL=$(echo "$R" | jq -r '.ideal_screenshot_url // "null"')
echo "$STATUS desktop_ideal=$([ "$IDEAL" = null ] && echo no || echo yes)"
[ "$STATUS" = completed ] && [ "$IDEAL" != null ] && break
sleep 30
done
Response-Shape (GET /page-audit/{id} nach completed):
{
"id": 10,
"tracking_project_id": null,
"url": "https://www.example.com/landing",
"status": "completed",
"error_message": null,
"screenshot_url": "https://rankion.ai/storage/page-audits/10/screenshot.png",
"ideal_screenshot_url": "https://rankion.ai/storage/page-audits/10/ideal.png",
"lighthouse": { "performance": 78, "seo": 92, "accessibility": 88, "best_practices": 83 },
"analysis": {
"user_intent": "1-Satz Search-Intent",
"persona_fit": 75,
"trust_score": 72,
"layout_score": 68,
"cta_score": 65,
"problem_solution_clarity": 70,
"above_the_fold_quality": 60,
"mobile_friendliness_visual": 80,
"personas": [
{ "name": "...", "who": "...", "primary_motivation": "...",
"pain_points": ["..."], "goals": ["..."],
"fit_score": 75, "fit_reason": "..." }
],
"critical_issues": [
{ "severity": "high|medium|low", "title": "...", "explanation": "...", "evidence": "..." }
],
"improvement_suggestions": [
{ "area": "headline|cta|trust|layout|copy|visuals|forms|navigation|seo|accessibility",
"priority": "high|medium|low",
"title": "...", "description": "...", "example": "before/after micro-copy" }
],
"summary": "3-5 Satz Executive-Summary",
"headline_rewrite": "stärkerer H1-Vorschlag oder null"
},
"gpt_image_used": true,
"credits_used": 75,
"started_at": "2026-04-27T19:28:58Z",
"completed_at": "2026-04-27T19:30:22Z"
}
Status-Codes: 202 (gestartet), 200 (Detail/List), 403 (Cross-Team / Cross-Project), 404 (Audit nicht gefunden), 422 (Validation: url Pflicht, max 500 Zeichen).
Use-Cases:
analysis.improvement_suggestions (sortiert nach priority) lesen → Änderungen am Source-Page anwenden → Re-Audit → Score-Diff vergleichen.ideal_screenshot_url (Desktop+Mobile) als Visual-Reference in den Design-Pipeline-Iteration füttern.analysis.personas[].pain_points + goals → gezielte Headline-/CTA-Varianten schreiben → re-audit.analysis.headline_rewrite ist ein validierter Headline-Replacement — kann direkt in den Live-Page übernommen werden.Auto-Generator für Tracking-Project-Reports (Markdown-Output via GenerateTrackingProjectReportJob) und ein Cross-Modul-Korrelations-Endpoint, der Site-Audit, Rank-Tracker, Backlinks und AVI verknüpft. Authentifizierung: Authorization: Bearer <token>, alle Endpoints team-scoped.
| Method | URL | Credits | Beschreibung |
|---|---|---|---|
| POST | /tracking-projects/{id}/generate-report |
10 | Report generieren (async). 202 + {report_id, tracking_project_id, status, status_endpoint}. Rate-Limit: 1 Request / 30 Min / Project — sonst 429 {error:"rate_limited"}. |
| GET | /tracking-projects/{id}/reports |
— | Letzte 50 Reports des Projects (sortiert nach created_at desc). |
| GET | /reports/{id} |
— | Report-Detail inkl. markdown_content und summary_json. |
Status-Werte: pending → generating → completed (oder failed).
Polling-Pattern:
# 1) Report dispatchen
RID=$(curl -s -X POST $BASE/tracking-projects/$PID/generate-report \
-H "Authorization: Bearer $TOKEN" | jq -r '.report_id')
# 2) Pollen bis completed
while true; do
S=$(curl -s -H "Authorization: Bearer $TOKEN" $BASE/reports/$RID | jq -r '.status')
echo "Status: $S"
[ "$S" = completed ] && break
[ "$S" = failed ] && exit 1
sleep 5
done
# 3) Markdown extrahieren
curl -s -H "Authorization: Bearer $TOKEN" $BASE/reports/$RID | jq -r '.markdown_content' > report.md
Response — POST /tracking-projects/{id}/generate-report (202):
{
"report_id": 42,
"tracking_project_id": 7,
"status": "pending",
"status_endpoint": "/v1/reports/42",
"message": "Report dispatched"
}
Response — GET /tracking-projects/{id}/reports (200):
{
"data": [
{
"id": 42,
"tracking_project_id": 7,
"team_id": 3,
"user_id": 12,
"status": "completed",
"credits_used": 10,
"generated_at": "2026-04-27T18:14:22+00:00",
"created_at": "2026-04-27T18:13:40+00:00",
"updated_at": "2026-04-27T18:14:22+00:00"
}
]
}
Response — GET /reports/{id} (200):
{
"id": 42,
"tracking_project_id": 7,
"team_id": 3,
"user_id": 12,
"status": "completed",
"markdown_content": "# Tracking-Report Project XY\n\n## Executive Summary\n…",
"summary_json": {
"headline_kpis": { "avi_score": 64, "delta_30d": "+8", "top10_keywords": 14 },
"wins": [ "..." ],
"risks": [ "..." ]
},
"credits_used": 10,
"error_message": null,
"generated_at": "2026-04-27T18:14:22+00:00",
"created_at": "2026-04-27T18:13:40+00:00",
"updated_at": "2026-04-27T18:14:22+00:00"
}
Status-Codes: 202 (Report dispatched), 200 (List/Detail), 403 (Cross-Team / Cross-Project), 404 (Report/Project nicht gefunden), 429 (Rate-Limit: max 1 / 30 Min).
Aggregiert drei Korrelations-Datenpunkte für einen TrackingProject in einem Roundtrip — Risk-Map (Site-Audit × Rank-Tracker), Backlinks-Velocity × AVI-Trend (Pearson, 30d), Smart-Todos.
| Method | URL | Credits | Beschreibung |
|---|---|---|---|
| GET | /tracking-projects/{id}/correlation |
— | Cross-Module-Korrelation Snapshot |
Response — GET /tracking-projects/{id}/correlation (200):
{
"data": {
"site_audit_ranking_risks": [
{
"keyword_id": 91,
"keyword": "best crm",
"search_volume": 4400,
"position": 5,
"url": "https://example.com/best-crm",
"issues_count": 7,
"critical_count": 2,
"medium_count": 3,
"risk_score": 8.4,
"issues": [
{ "type": "missing_h1", "severity": "critical", "message": "..." }
]
}
],
"backlinks_av_correlation": {
"correlation_coefficient": 0.612,
"days_with_data": 14,
"velocity_total_30d": 23,
"avi_data_points_30d": 18,
"backlink_events_30d": 41,
"observation": "Moderat positive Korrelation: Backlink-Velocity beeinflusst AVI."
},
"smart_todos": [
{
"priority_score": 84,
"estimated_impact": "medium",
"title": "Page-Issues fixen die Top-Ranking gefährden",
"why": "URL …/best-crm rankt für \"best crm\" auf Position 5 — hat aber 7 Site-Audit-Issues (missing_h1, ...).",
"reference_type": "tracking_keyword",
"reference_id": 91,
"url": "https://example.com/best-crm",
"keyword": "best crm",
"position": 5,
"risk_score": 8.4
}
]
}
}
Status-Codes: 200 (Erfolg), 403 (Cross-Team / Cross-Project), 404 (Project nicht gefunden).
# 1) Analyse starten
RESP=$(curl -s -X POST $BASE/tracking-projects/analyze \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"mode":"domain","domain":"example.com","language":"de","country":"DE"}')
PID=$(echo $RESP | jq -r .project_id)
# 2) Polling bis completed
while [ "$(curl -s $BASE/tracking-projects/$PID/analysis-status \
-H "Authorization: Bearer $TOKEN" | jq -r .analysis_status)" != "completed" ]; do
sleep 3
done
# 3) Atomic Aktivierung
curl -X POST $BASE/tracking-projects/$PID/activate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"keywords":["best crm","crm software"],
"competitors":[{"domain":"hubspot.com"}],
"prompts":[{"prompt":"What is the best CRM?","category":"recommendation"}],
"platforms":["chatgpt","perplexity"],
"frequency":"weekly",
"with_search":true,
"reality_check_enabled":true
}'
curl "$BASE/explorer/176007?include=related,questions" \
-H "Authorization: Bearer $TOKEN"
# returns: keyword, search_volume, kd_score, parent_topic, traffic_potential,
# serp_urls, monthly_searches[12], related[90], questions[11], cache_hit
# Generate
curl -X POST $BASE/articles/$ART/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"keyword":"AI coding tools","article_type":"blog","target_length":1500}'
# Publish to WordPress (CMS-ID 1 vorausgesetzt)
curl -X POST $BASE/articles/$ART/publish \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"cms_integration_id":1}'
# Liste mit Filter
curl "$BASE/tracking-projects/$PID/cited-sources?outreach_status=neu" \
-H "Authorization: Bearer $TOKEN"
# Status updaten
curl -X PATCH $BASE/tracking-projects/$PID/cited-sources/$SID \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"outreach_status":"anfrage_gestellt","notes":"E-Mail an redaktion@..."}'
# CSV-Export
curl "$BASE/tracking-projects/$PID/cited-sources/export?from=2026-01-01" \
-H "Authorization: Bearer $TOKEN" -o sources.csv
credits:N und/oder Job-Dispatch geben 202 Accepted mit Job-ID. Polling via Detail-Endpoint bis processing_status == 'ready'.?cache=false (z.B. Explorer). Inline 50 Credits Charge.?from=&to=&status=&platform=&search=&sort=&limit=&page=.?per_page=N&page=N. Response: {data:[], meta:{total, per_page, current_page, last_page}}.?include=related,questions (kommagetrennt) inlined Sub-Resources statt separater Roundtrips.Diese Datei spiegelt routes/api.php. Die maschinenlesbare SSOT der /v1-Route-Liste
ist docs/API_ROUTES.generated.json — pro Endpoint method, uri (doc-Stil, ohne
api-Prefix), name, action (Controller@method | Closure) und der aus der
Middleware geparste credits + credits_mode (gate = credits:N, auto =
credits-auto:N). Sortiert nach (uri, method), diff-stabil.
# Manifest (neu) generieren — nach jeder Route-Änderung ausführen + committen:
php artisan api:generate
# CI-Gate: prüft (ohne zu schreiben), ob das committete Manifest aktuell ist.
# Exit 1 + Diff-Summary bei Drift:
php artisan api:generate --check
# Schnellzählung der Live-/v1-Endpoints (Fallback):
php artisan route:list | grep "api/v1" | wc -l
Quelle: App\Services\Api\RouteIntrospector (spiegelt exakt
ApiDocsController::countV1Endpoints()). Der Parity-Test
tests/Feature/Docs/ApiDocParityTest.php erzwingt: (1) Introspector-Count ==
Controller-Count, (2) committetes Manifest aktuell (sonst „Run php artisan api:generate"), (3) jede /v1-Route ist in dieser Datei dokumentiert ODER auf der
expliziten KNOWN_UNDOCUMENTED-Baseline (fängt neue undokumentierte Routen), (4)
jede in dieser Datei genannte /v1-Pfad-Referenz löst auf eine echte Route auf, (5)
Modul-Doc-Freshness (README-Link↔Datei-Parität + Soft-Check der /api/v1-Referenzen
in docs/modules/*.md).
Live-Tabellen mit Beispielen: https://rankion.ai/settings/api-documentation
Public-share API for AI chat sessions. Owner creates a share link from /api/v1/chat-shares, recipient views it via /share/{token} (public, noindex). All endpoints require auth:sanctum and team-scoping.
| Method | URL | Description | Credits |
|---|---|---|---|
| GET | /v1/chat-shares |
List own share links (paginated, 25/page) | 0 |
| POST | /v1/chat-shares |
Create new share for owned session (default 7-day expiry) | 0 |
| GET | /v1/chat-shares/{id} |
Detail + stats | 0 |
| PATCH | /v1/chat-shares/{id} |
Update settings | 0 |
| DELETE | /v1/chat-shares/{id} |
Revoke (soft delete; row preserved) | 0 |
| GET | /v1/chat-shares/{id}/views |
Paginated view log (DSGVO-anonymized) | 0 |
| GET | /v1/agentic/sessions/{session}/shares |
Shares for a specific session | 0 |
Body:
session_id (required, integer): id of an AiChatSession owned by the authenticated usertitle (optional, string)expires_at (optional, ISO datetime, must be future)password (optional, string min 4)max_views (optional, integer min 1)show_user_messages (default true)show_assistant_messages (default true)show_tool_results (default false — recipients see only inputs+outputs, not the tool-call cards)show_reasoning_steps (default false)show_uploaded_files (default true)true, else 422.Response 201:
{
"data": {
"id": 1, "token": "<40 chars>",
"url": "https://rankion.ai/share/<token>",
"title": "...", "session_id": 42,
"expires_at": "...", "frozen_at": null,
"has_password": false, "max_views": null,
"view_count": 0, "unique_viewer_count": 0,
"last_viewed_at": null, "revoked_at": null,
"status": "active",
"show_user_messages": true,
"created_at": "..."
}
}
Same fields as POST plus freeze: bool (true = freeze content snapshot, false = back to live).
Soft revoke. Sets revoked_at. Public access returns 410. Row stays in DB until daily prune job (90 days post-revoke by default, configurable via CHAT_SHARES_RETENTION_DAYS).
GET /share/{token} — public, no auth, returns HTML. noindex enforced via meta + X-Robots-Tag header + robots.txt disallow.
POST /share/{token}/unlock — password-form submission; throttle 5/min. Sets path-scoped httpOnly cookie on success.
GET /share/{token}/print — clean print-CSS variant of the share view.
Read-only API for the agentic-superpowers Brainstorm → Plan → Execute pipeline. Plans are created/mutated EXCLUSIVELY by the master agent's pipeline tools (build_plan, complete_step, fail_step, skip_step, complete_plan, clarify_intent); the HTTP API exposes only inspection. All endpoints require auth:sanctum and team-scoping (Policy: view only — no update/delete exposed).
| Method | URL | Description | Credits |
|---|---|---|---|
| GET | /v1/agentic/sessions/{session}/plans |
Plan history for a session (paginated 50/page, ordered desc by created_at) | 0 |
| GET | /v1/agentic/plans/{plan} |
Plan detail (status, denormalized counters, brainstorm_summary) | 0 |
| GET | /v1/agentic/plans/{plan}/steps |
All steps with output_summary, output_data (≤64kB JSON), tool_calls_made, credits_used, latency_ms, retry_count, last_error, subagent_session_id | 0 |
| POST | /v1/agentic/feedback |
Persists 👍/👎 feedback on an assistant message. Body: {message_id: int, direction: 'up'|'down'}. Idempotent upsert keyed by (user_id, message_id) — second click overwrites direction. Returns 404 unknown_msg, 403 not_yours, 422 invalid direction. Response: {ok: true, direction} |
0 |
{
"data": {
"id": 12, "session_id": 42, "goal": "Konkurrenz-Analyse example.com",
"brainstorm_summary": null, "strategy": "linear",
"status": "executing",
"current_step_id": 35,
"total_steps": 4, "completed_steps": 1, "failed_steps": 0,
"started_at": "2026-05-01T08:30:00Z", "completed_at": null,
"total_tool_calls": 9, "total_credits_used": 5,
"created_at": "2026-05-01T08:29:55Z"
}
}
{
"data": [
{
"id": 35, "step_index": 0,
"label": "Top-10 SERP-Konkurrenten ermitteln",
"description": "...",
"complexity": "trivial", "assigned_agent": null,
"status": "completed", "retry_count": 0, "last_error": null,
"output_summary": "10 Konkurrenten: ahrefs.com, semrush.com, ...",
"output_data": { "competitors": ["ahrefs.com", "..."] },
"tool_calls_made": 1, "credits_used": 1, "latency_ms": 3120,
"subagent_session_id": null,
"started_at": "...", "completed_at": "..."
}
]
}
status values: pending | in_progress | completed | failed | skipped (steps); planning | executing | completed | failed | aborted (plans).
assigned_agent values: null (master itself) | master | analyze | explore | research | action (Subagent-Persona).
Maschinenlesbarer Vertrag für Subsystem A + C — fixed 2026-05-09.
GET /api/v1/openapi.yamlOpenAPI 3.1.0 Spec (public, kein Auth nötig). 5-min CDN-Cache. Generiert TypeScript/Python/PHP-SDKs aus z.B. openapi-generator-cli:
npx @openapitools/openapi-generator-cli generate \
-i https://rankion.ai/api/v1/openapi.yaml \
-g typescript-fetch \
-o ./rankion-sdk-ts
Schema-Coverage:
/grounding/analyze, /audits/{id}, /audits, /batch, /batches/{id}, /batches/{id}/results.ndjson, /sitemap-audit/content-optimizer/{id} (canonical + alias)/agentic/chat, /agentic/sessions/{session}GroundingAudit, GroundingBatch, Finding, Tier, StructuredError, AgenticChatRequest/Response, AgenticSessionStatusAndere Endpoints sind weiterhin nur in dieser Markdown-Reference dokumentiert; eine vollständige OpenAPI-Export-Phase ist auf der Roadmap.
Alle code Werte aus dem error: {code, ...} Envelope (Subsystem A + C):
| HTTP | code | Endpoint | Beschreibung |
|---|---|---|---|
| 422 | context_overflow |
/agentic/chat | 1M-Token-Limit überschritten. Body: limit_tokens, used_tokens, suggested_action, max_urls_per_call |
| 502 | anthropic_api_error |
/agentic/chat | Anthropic-Upstream 5xx. Body: http_status, message |
| 404 | audit_not_found |
/grounding/audits/{id} | ID unbekannt ODER fremdes Team (404 statt 403 gegen Enumeration) |
| 410 | audit_expired |
/grounding/audits/{id} | TTL gepruned (30d soft + 60d hard) |
| 404 | batch_not_found |
/grounding/batches/{id} | ID unbekannt ODER fremdes Team |
| 422 | invalid_url |
/grounding/* | Kein gültiger http(s)-URL |
| 422 | invalid_framework |
/grounding/* | Framework außerhalb [v1.5, eeat, people-first] |
| 422 | too_many_urls |
/grounding/batch | Batch > 500 URLs |
| 402 | insufficient_credits |
/grounding/, /content-optimizer/ | Team-Balance < required |
| 503 | sitemap_unreachable |
/grounding/sitemap-audit | Sitemap-Parse fehlgeschlagen |
| 404 | session_not_found |
/agentic/sessions/{id} | Triple-scoped IDOR-Schutz greift |
| 429 | rate_limited |
(alle Endpoints mit throttle:) |
Standard Laravel-Throttle. Header Retry-After setzt Wartezeit |
Jeder Endpoint mit throttle:N,M Middleware liefert auf erfolgreiche Responses:
X-RateLimit-Limit: N (Anfragen pro Window)X-RateLimit-Remaining: <int> (verbleibend im aktuellen Window)X-RateLimit-Reset: <epoch> (Unix-Timestamp wann Counter resettet)Bei 429: zusätzlich Retry-After: <seconds>. Counters per Sanctum-Token, nicht per IP.
Throttle-Werte pro Endpoint stehen in der Endpoint-Tabelle der jeweiligen Sektion (oben). Aktuelle Werte:
/grounding/analyze: 30/min · /grounding/batch, /sitemap-audit: 5/min · /grounding/audits/{id}, /batches/{id}: 120/min · /grounding/audits (Liste): 60/min · /grounding/batches/{id}/results.ndjson: 30/min · /agentic/sessions/{session}: 120/min · /agentic/chat: kein expliziter Throttle (Cap durch interne max_iterations + Anthropic-Token-Limits).Drei Härtungen am /api/v1/agentic/chat Flow für robustere programmatische Caller — fixed 2026-05-09.
context_overflow ErrorWenn der Agent das 1M-Token-Kontext-Limit hittet, returnt /agentic/chat jetzt HTTP 422 mit strukturiertem Error-Envelope statt 200+German-Text:
{
"session_id": 1201,
"error": {
"code": "context_overflow",
"limit_tokens": 1000000,
"used_tokens": 1042000,
"suggested_action": "split_request",
"max_urls_per_call": 1
},
"text": "⚠️ Diese Anfrage ist zu komplex geworden …"
}
Programmatischer Retry ohne String-Matching auf Deutsch. Bei Anthropic-API-Errors (5xx upstream) → HTTP 502 mit error.code = "anthropic_api_error".
tool_policy im RequestRestriktiere welche Tools der Master-Agent in einem Turn nutzen darf — ideal für Audit-Loops, die nur 1-Tool-Calls wollen ohne Token-Blow:
{
"message": "Audit https://example.com",
"tool_policy": {
"allow": ["analyze_grounding_page"],
"deny": ["web_scrape", "extract_from_urls"],
"max_tool_calls": 1
}
}
Server-side Enforcement: gesperrte Tools liefern Tool-Result {success:false, error:"tool X blocked by tool_policy.deny", policy_blocked:true} — der Agent ignoriert sie und liefert ein Teil-Resultat statt das Limit zu sprengen.
/agentic/chat blockiert standardmäßig NICHT mehr bis zum finalen Resume. Wenn der Agent mid-turn wait_and_recheck ausgelöst hat, returnt der Endpoint HTTP 202 sofort:
{
"session_id": 1201,
"agentic_status": "waiting",
"eta_seconds": 120,
"poll_url": "/api/v1/agentic/sessions/1201",
"resumed": false,
"text": "⏳ Ich warte auf Hintergrund-Daten…"
}
Caller polled dann GET /api/v1/agentic/sessions/{id}:
{
"session_id": 1201,
"agentic_status": "completed",
"message_id": 4567,
"text": "Audit fertig: Score 75/100…",
"last_message_at": "2026-05-09T14:32:01+00:00",
"is_terminal": true
}
agentic_status: null (idle) | running | waiting | awaiting_user | failed | completed. is_terminal=true heißt: keine weitere Hintergrund-Arbeit pending (awaiting_user ist NICHT terminal — es wartet auf eine User-Antwort).
Opt-in legacy blocking: POST /agentic/chat?wait_for_final=true → server polled bis 480s und liefert das finale Result inline (alte Verhalten).
ask_user_question, 2026-06-10)Ruft der Agent das ask_user_question-Tool (oder ein Plan-Confirmation-Gate), pausiert die Loop mit agentic_status='awaiting_user'. POST /agentic/chat, GET /agentic/sessions/{id} und POST /agentic/sessions/{id}/answer-question hängen dann zusätzlich an:
{
"agentic_status": "awaiting_user",
"is_terminal": false,
"awaiting_user": true,
"pending_question": "Soll ich den teuren Voll-Pull starten (120 Credits)?",
"pending_question_options": [
{"label": "Ja, starten (Empfohlen)", "description": "Kostet 120 Credits"},
{"label": "Nein, abbrechen", "description": ""}
],
"answer_url": "/api/v1/agentic/sessions/1201/answer-question"
}
Antworten: POST /agentic/sessions/{id}/answer-question mit { "option_label": "Ja, starten (Empfohlen)" } → resumed die Loop synchron (Response-Shape = /agentic/chat; enthält erneut die awaiting_user-Felder, falls direkt eine Folge-Frage kommt). Damit funktionieren Rückfragen + Aktions-Bestätigungen via API genauso wie im Chat-UI. Die Entscheidung WANN der Agent fragt ist advisory (Tool-Beschreibung empfiehlt es bei >50-Credit-Ops) — kein erzwungener Gate.
Async-Pipeline für LLM-Citation-Readiness-Audits über öffentliche URLs. Single + Bulk + Sitemap mit NDJSON-Stream und HMAC-signierten Webhook-Callbacks.
Frameworks:
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| POST | /v1/grounding/analyze |
Single-URL-Audit dispatchen. Body: {url, frameworks?, tracking_project_id?}. Throttle: 30/min. Returns 202 + {audit_id, status, estimated_seconds, poll_url} + Location: Header. |
1 |
| GET | /v1/grounding/audits/{id} |
Polling/Result. Returns {audit_id, status, score, tier:{name,threshold_min,threshold_max}, frameworks{}, findings[], raw_text, duration_ms, credits_used, started_at, completed_at, expires_at, ui_url}. ui_url (string) = relativer UI-Pfad /grounding/audits/{id} — kanonischer Detail-Link fürs Team (nie selbst konstruieren). 404 (audit_not_found), 410 (audit_expired). |
0 |
| GET | /v1/grounding/audits |
Liste eigener Audits (jede Row inkl. ui_url). Filter: ?status=, ?tracking_project_id=, ?from=, ?to=, Pagination. |
0 |
| POST | /v1/grounding/batch |
Bulk-Audit für 1–500 URLs. Body: {urls[], frameworks?, concurrency? (1-10), callback_url?, tracking_project_id?}. Throttle: 5/min. Returns 202 + {batch_id, url_count, callback_secret, eta_seconds, poll_url, results_url}. callback_secret einmalig. |
url_count |
| GET | /v1/grounding/batches/{id} |
Batch-Status + Counters. Returns {batch_id, status, source, completed, failed, total, concurrency, eta_seconds, results_url, callback?, started_at, completed_at}. |
0 |
| GET | /v1/grounding/batches/{id}/results.ndjson |
NDJSON-Stream completed/failed Audit-Rows. Streamt während Lauf. | 0 |
| POST | /v1/grounding/sitemap-audit |
Sitemap-Audit. Body: {sitemap_url, frameworks?, filter:{path_contains?,limit? (max 500),priority_min?}, concurrency?, callback_url?, tracking_project_id?}. Credits werden NACH Sitemap-Parse abgezogen. |
url_count_after_parse |
| Score | Tier |
|---|---|
| 0–25 | not-grounded |
| 26–50 | weak |
| 51–75 | partial |
| 76–100 | grounded |
Jedes Audit liefert findings[] mit Objekten: {id, severity (critical|high|medium|low|info), framework (v1.5|eeat|people-first), title, description, fix:{type, action, details}, spec_url}. Zusätzlich raw_text (Markdown) für UI-Render.
Server POSTet zu callback_url mit Header X-Rankion-Signature: sha256=<hex> (HMAC-SHA256 über Body, Key = callback_secret) + X-Rankion-Webhook-Id: <uuid>. 4 Retries (60/300/1800/7200s) bei 5xx oder 429. Verifikation:
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $callbackSecret);
hash_equals($expected, $request->header('X-Rankion-Signature'));
| HTTP | code |
|---|---|
| 422 | invalid_url, invalid_framework, too_many_urls |
| 402 | insufficient_credits |
| 404 | audit_not_found, batch_not_found |
| 410 | audit_expired |
| 503 | sitemap_unreachable |
| 429 | rate_limited |
Beispiel — Single-URL Audit:
ID=$(curl -sX POST -H "Authorization: Bearer $RANKION_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","frameworks":["v1.5"]}' \
https://rankion.ai/api/v1/grounding/analyze | jq -r .audit_id)
# Poll
sleep 30
curl -H "Authorization: Bearer $RANKION_API_TOKEN" \
https://rankion.ai/api/v1/grounding/audits/$ID
Beispiel — Sitemap-Batch:
curl -X POST -H "Authorization: Bearer $RANKION_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"sitemap_url":"https://example.com/sitemap.xml","filter":{"path_contains":"/blog","limit":50},"callback_url":"https://my-app/hook"}' \
https://rankion.ai/api/v1/grounding/sitemap-audit
Knowledge-Base intern: docs/knowledge/grounding-pages-geo.md. Modul-Doku: docs/modules/grounding-audit.md.
Read-only Discovery-Endpoint für die GEO-/LLM-Citation-Kriterien — die Single Source of Truth (App\Support\Geo\GeoCriteriaRegistry), die den Grounding-Audit, den GEO-Content-Score, die Generierungs-Prompts (ContentGeneratorService — live) und die Off-Page-/Demand-Achse (OffPageReadinessScorer) speist. Lässt GEO-Optimizer-Agenten entdecken, welche Signale Rankion prüft, mit welchem Engine-Signal, welcher Evidenz-Stufe und an welchen Oberflächen (audit/score/prompt/offpage). Synchron, 0 Credits, kein Polling.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /v1/geo-criteria |
GEO-Kriterien-Katalog. Optional ?category=<cat> (z.B. entity, extraction, schema, evidence, eeat, indexability, offpage). Returns {data: [{id, label, description, category, engine_signal, dimension, evidence_tier, study_source, score_weight, shapes[]}]}. Read-only. |
0 |
| GET | /v1/tracking-projects/{id}/off-page-readiness |
Off-Page-/Demand-Achsen-Score eines Tracking-Projekts. Team-scoped, read-only. | 0 |
Response-Felder (/v1/geo-criteria):
id — stabile Kriterien-ID, identisch mit der Grounding-Finding-ID bzw. dem GEO-Score-Breakdown-Key (gp.h1.entity_only, geo.evidence.citation_density, geo.offpage.brand_demand, …).engine_signal — referenziertes Engine-Capability-Matrix-Signal (config/engines.php).dimension — technical (Signal indexability/crawlability) oder content. Abgeleitet, nie driftend.evidence_tier — A/B Default-Hint. Der wirksame Tier eines konkreten Findings kommt aus /v1/grounding/audits/{id} (Laufzeit-Auflösung über die Engine Capability Matrix).shapes[] — Oberflächen, an denen das Kriterium teilnimmt: audit (Page- oder Site-Root-Detektor), score, prompt, offpage (Demand-Achse, geo.offpage.*).GET /v1/tracking-projects/{id}/off-page-readiness — aggregierter Off-Page-Score eines Tracking-Projekts (gewichtetes Mittel über die data_available-Signale). Team-scoped: Fremd-Team → 403, unbekannte ID → 404. 0 Credits.
{
"score": 87,
"data_available": true,
"components": {
"geo.offpage.brand_demand": { "score": 100, "data_available": true, "meta": { "brand_keyword": "rankion", "search_volume": 12000 }, "weight": 25 },
"geo.offpage.third_party_mentions": { "score": 70, "data_available": true, "meta": { "third_party_count_90d": 30, "window_days": 90 }, "weight": 20 },
"geo.offpage.source_affinity": { "score": 100, "data_available": true, "meta": { "distinct_third_party_types": 3, "has_wiki": true }, "weight": 15 }
},
"meta": { "tracking_project_id": 17, "credits": 0 }
}
score ist int|null — null ⇒ noch keine Daten, NICHT 0. data_available unterscheidet beide Fälle; jedes components.* trägt sein eigenes data_available + meta.reason.brand_demand braucht brand_name + passendes Tracking-Keyword mit search_volume; third_party_mentions braucht aktivierten Community-Monitor (CommunityProject am selben Project); source_affinity braucht Citation-Historie (cited_sources).community_project_id (Drittseiten-Mentions) vs. tracking_project_id (Brand-Demand + Quellen-Affinität) pro Faktor auf — via CommunityProject.project_id == TrackingProject.project_id.Accept: application/vnd.rankion.v2+json): {data: {score, data_available, components}, meta}.Read-only: Detector-/Factor-Objekte werden nie serialisiert — nur skalare Metadaten. Den Katalog einmal ziehen, dann damit Grounding-Audit (/v1/grounding/analyze), Content-Optimizer bzw. Off-Page-Readiness ansteuern.
Skill-Manifeste: grounding.geo-criteria, tracking.off-page-readiness. Modul-Doku: docs/modules/geo-criteria-catalog.md.
Public + internal wiki content. Markdown-Files in docs/wiki/ werden via php artisan wiki:sync (auto bei jedem Deploy) in die DB gespiegelt. Slug-Hierarchie via /: modules/backlinks. Wiki-interne Links via [[slug]] oder [[slug|Anzeigetext]].
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /v1/wiki/pages |
Liste aller Pages (paginiert 50/Seite). Filter: ?category=modules, ?visibility=public|internal. Sanctum-User sehen auch internal-Pages. |
0 |
| GET | /v1/wiki/pages/{slug} |
Single Page mit body_md, body_html, toc[] (level/text/anchor), related[]. Slug akzeptiert Slashes für Hierarchie. |
0 |
| GET | /v1/wiki/search?q=... |
Top-20 FULLTEXT-Treffer (title+description+body_md). Title-Boost 2×. Snippets mit <mark>-highlighted Tokens. Min. 2 Zeichen. |
0 |
Public-Reader: https://rankion.ai/wiki/{slug} — server-rendered HTML, Sidebar+TOC, /-Shortcut für Search-Focus. visibility: internal → 401 für anonyme User.
Master-Agent-Integration: query_wiki(query) Master-Tool returnt Top-3 mit vollen URLs — Trigger bei "wie geht X", "wie aktiviere ich Y", "was ist Z?".
Tiefenanalyse über alle 6 Mining-Schichten (Atomisierung → Quellenscan → Cross-Pattern → Faktencheck → Forecast → Action). Liefert pro Projekt einen 9-Sektionen-Report (Executive Summary, Faktenfehler, Konsens-Narrative, Persona-Lücken, Outreach Top-50, Wettbewerbs-Co-Occurrence, Frühwarn-Radar, Source-Whitelist, 6-Wochen-Forecast). Voraussetzung: Atomisierung läuft (Layer 1 backfill); für Faktencheck zusätzlich Truth-Source mit human_reviewed=true.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /v1/data-mining/insights/{project} |
Voller 12-Sektionen-JSON-Payload (alle Sections in einer Response). | 0 |
| POST | /v1/data-mining/insights/{project}/refresh |
Triggert Insights-Report-Generierung (PDF + CSV-ZIP-Bundle, asynchron). HTTP 202. | 0 |
| GET | /v1/data-mining/insights/{project}/section/{section} |
Einzel-Section-Lazy-Load. {section} ∈ executive_summary, fact_errors, consensus_narratives, consensus_clusters, persona_gaps, battle_cards, outreach_top50, competitor_cooccurrence, early_warnings, source_whitelist, forecast_6w, recommended_actions. |
0 |
| GET | /v1/data-mining/exports/{project}.pdf |
Letzten generierten PDF-Export downloaden. 404 falls noch keiner existiert (vorher refresh aufrufen). |
0 |
| GET | /v1/data-mining/exports/{project}.zip |
Letztes CSV-Bundle (1 CSV pro Section, gezippt). 404 wenn nicht vorhanden. | 0 |
Alle Endpoints Sanctum-authed mit Team-Scope-Check (tracking_projects.team_id === user.currentTeam->id, sonst 403).
Wöchentlicher Insights-Report: Jeden Sonntag 18:00 wird für alle Projekte der PDF-Report generiert und mit Highlights-Summary an den Team-Owner gemailt.
Live-Dashboard: https://rankion.ai/ai-visibility/{id}/insights — interaktive 9-Tab-Ansicht des gesamten Payloads.
Layer-7-Engine über tracking_cited_sources: extrahiert pro Citation strukturierte Dimensionen (Content-Format, Schema-Types, E-E-A-T-Signale, Pricing/FAQ-Indikatoren, Domain-Authority), vergleicht gegen Google-SERP-Top-10-Counterfactuals und produziert Lift-Scores als Hebel-Empfehlungen. Drei Tiers: per Project (Tier 1), Account (Tier 2), Admin Global (Tier 3).
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /v1/citation-causality/project/{projectId}/patterns |
Alle Pattern-Rows (Dimension × Wert × Lift-Score × p-Value) für ein Projekt. Lift>1 = überproportional bei zitierten Pages. | 0 |
| GET | /v1/citation-causality/project/{projectId}/recommendations |
Priorisierte Hebel-Karten P1-P4 nach Lift × Signifikanz × Sample-Size; mit description (LLM-Klartext) und Top-3 Evidence-Citations. |
0 |
| GET | /v1/citation-causality/project/{projectId}/audit?url=<sitemap_url> |
Per-Page-Compliance-Audit: extrahiert Dimensionen der URL via Ollama, vergleicht mit Top-Patterns, liefert Gap-Liste + estimated_uplift. | 1 |
| POST | /v1/citation-causality/project/{projectId}/audit/bulk |
Bulk-Audit async für bis zu 200 URLs. Body {urls: [...]}. Returnt 202 + audit_id. Job läuft auf Queue ollama (ca. 30s pro URL). |
10 |
| GET | /v1/citation-causality/project/{projectId}/audit/bulk/{auditId} |
Status-Poll für Bulk-Audit. Returns status (pending/processing/completed/failed) + results (Array von Per-URL-Resultaten). |
0 |
| GET | /v1/citation-causality/account/cross-project |
Cross-Project-Patterns über alle Projekte des Account-Teams. Nutzt für Tier-2-Dashboard "Welcher Hebel funktioniert wo?". | 0 |
| GET | /v1/citation-causality/admin/global |
Globale Patterns über alle Customers/Branchen. Admin-only (401 sonst). Speist Tier-3-Strategie-Dashboard. | 0 |
| GET | /v1/citation-causality/admin/sandbox/runs |
Liste aller Sandbox-Runs (paginiert 25). Admin-only. Felder: name, status, total_citations_target, started_at, completed_at, estimated_cost_cents. | 0 |
| POST | /v1/citation-causality/admin/sandbox/runs |
Neuen Sandbox-Run starten. Body: {name, catalog_id, ollama_model_primary, ollama_model_fallback, prompt_template_version, citation_filter:{project_ids,limit}, serp_counterfactual_enabled, dataforseo_domain_analytics_enabled}. Returnt 202 + run_id. |
0 |
| GET | /v1/citation-causality/admin/sandbox/runs/{runId} |
Run-Detail mit verschachtelten Citations + Counterfactuals + Patterns. Admin-only. | 0 |
| POST | /v1/citation-causality/admin/sandbox/runs/{runId}/promote |
Promote eines Sandbox-Runs nach Production (Plan 3). Aktuell 501 bis Plan 3 implementiert. | 0 |
| GET | /v1/citation-causality/projects/{project}/recipe |
Plan 5 Phase E — Content-Recipe (briefing + 3-5 title_suggestions + bullets + CTA + confidence) für das Team via ContentRecipeSynthesizer. 24h Cache (key: cce.recipe.t<id>.<sha>). Bei Top-3 Hebel n_cited<5 → {available:false, reason:"insufficient_sample"}. |
0 |
| GET | /v1/citation-causality/projects/{project}/levers |
Plan 5 Phase E — {top, unique, anti} Hebel-Buckets mit Klartext-Labels (key_label, value_label) + interpretation (headline, explanation, action). Optional ?include=trend,by_llm lazy-expansion pro Lever. Bounded auf 30+20+15 Rows. |
0 |
| GET | /v1/citation-causality/projects/{project}/levers/{key}/{value}/drill-down |
Plan 5 Phase E — Per-Lever Tiefenansicht: details (cited URLs + SERP-Counterfactuals + counts), trend (Time-Axis-Verlauf), by_llm (Per-LLM-Provider Lift-Vergleich). Path-Param key regex [a-z][a-z0-9_]{0,63}. |
0 |
| POST | /v1/citation-causality/projects/{project}/audit |
Plan 5 Phase E — Dispatched AuditTopPagesAgainstHebelJob für Top-N own-domain URLs gegen Top-5 Hebel. Body optional {limit: 10} (1-50, default 10). Returnt 202 + {job, project_id, team_id, estimated_cost_credits}. Polling via Bestehender /audit/bulk/{auditId}. |
5 |
Alle Endpoints Sanctum-authed + Team-Scope (tracking_projects.team_id === user.currentTeam->id).
Skill-Caller-Idiom (für Master-Agent / Claude Code Skill / externe Agenten via REST):
POST /admin/sandbox/runs mit citation_filter:{project_ids:[N], limit:30} + serp_counterfactual_enabled:trueGET /admin/sandbox/runs/{runId} alle 30s bis status: completed (Run dauert 5-15 min)/admin/citation-causality-lab/catalog für Klon + Anpassung, dann neuer Run mit dimensions_catalog_id: <neue_id>cc_patterns + Klartext-Insights aus synthesized_text-Spalte. P1-P4-Priorisierung via /project/{projectId}/recommendations.Skill-Caller-Idiom Plan-5-Phase-E (Insight-to-Action über REST):
GET /projects/{id}/recipe — Cache-Hit liefert sofort, sonst <30s glm-4.7 CallGET /projects/{id}/levers für Top-Buckets, optional ?include=trend,by_llm für detaillierte InspectionGET /projects/{id}/levers/{key}/{value}/drill-down für Evidence (cited URLs + SERP-Counterfactuals)POST /projects/{id}/audit (5 Credits) → 202 + Job-Marker; Status via existierenden /audit/bulk/{auditId} Endpoint pollenavailable:false); Drill-Down auf bad-format Key → 404Live-Dashboards:
https://rankion.ai/ai-visibility/{projectId}?activeTab=citation-causalityhttps://rankion.ai/account/citation-intelligencehttps://rankion.ai/admin/citation-causality-intelligenceTeam-aggregiertes Markov-„Next-Route"-Modell für die intelligente Prefetch-Schicht (siehe docs/modules/turbo-prefetch.md). Team-gescopt, auth:sanctum (First-Party via Session-Cookie).
| Methode | URL | Beschreibung | Credits |
|---|---|---|---|
| POST | /v1/prefetch/transition |
Navigations-Übergang melden. Body: {from, to} (interne Pfade ^/…, kein Query/Signatur; from≠to). firstOrCreate+increment. 422 bei ungültigen Pfaden/Self-Loop. Throttle 60/min. |
0 |
| GET | /v1/prefetch/predictions?from=&limit= |
Top-N team-aggregierte Folge-Routen für from. Returns {from, predictions:[{to_route, count, prob}]} (prob = count/Σcount, sortiert desc). limit 1–10 (Default 5). |
0 |
| GET | /v1/prefetch/config |
Client-Config: {enabled, weights, limit, prerender_allowlist}. |
0 |
Workflow: nach jeder Navigation transition melden (fire-and-forget) → beim Einstieg predictions?from=<pfad> lesen → client-seitig mit lokalem localStorage-Markov blenden (lokal führend, Server = Cold-Start). Kein Polling, keine Job-Lifecycles.
User-scoped (NICHT team-scoped) — Konto-Sicherheitsstatus lesen, Passwort ändern, andere Sitzungen beenden. auth:sanctum, gelöst über den authentifizierten User ($request->user()), kein Team-Scope.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /account |
Sicherheitsstatus: {id, name, email, email_verified, two_factor_enabled, auth_provider, pending_email}. Enthält NIEMALS Secrets (kein two_factor_secret, keine recovery_codes, kein pending_email_token). |
— |
| PUT | /account/password |
Passwort ändern. Body: {current_password, password, password_confirmation}. current_password ist serverseitig Pflicht (422 sonst) — ein Token allein reicht NICHT. Google-Konten → 422 (Passwort per Reset-Link). throttle:10,1. |
— |
| DELETE | /account/sessions |
Beendet alle anderen Sitzungen des Users (löscht nur dessen eigene sessions-Rows außer der aktuellen). Response: {message, deleted}. throttle:10,1. |
— |
| GET | /account/notification-preferences |
Benachrichtigungs-Präferenz-Matrix (Default-aufgelöst): {data:{email:{reports,tracking_alerts,job_results}, inapp:{…}, locked:{billing,security,team}}}. email/inapp = abbestellbare Kategorien als bool (Default true); locked = immer-aktive Kategorien. |
— |
| PUT | /account/notification-preferences |
Präferenzen setzen. Body: {email?:{reports?:bool, tracking_alerts?:bool, job_results?:bool}, inapp?:{…}}. NUR optionale Kategorien werden berücksichtigt (nicht-optionale billing/security/team werden ignoriert). Es werden NUR Abweichungen (false) persistiert; alles-an ⇒ NULL. Antwort = GET-Shape (round-trip). |
— |
2FA-Setup/-Deaktivierung, E-Mail-Änderung und Konto-Löschung sind absichtlich nur in der Web-Oberfläche verfügbar, nicht über die REST-API. Begründung: Ein geleakter Sanctum-Token darf weder 2FA abschalten noch die E-Mail-Adresse übernehmen noch das Konto löschen können. Die Passwort-Änderung ist die einzige sicherheitskritische Schreib-Operation per API und verlangt deshalb zusätzlich das aktuelle Passwort (current_password).
Use-Case: Programmatischer Self-Service-Check + Härtung des eigenen Kontos (Status lesen, Passwort rotieren, fremde Sessions kappen) ohne Web-UI.
Idiom: GET /account (Status lesen — two_factor_enabled/email_verified prüfen) → bei Bedarf PUT /account/password (mit current_password + neuem Passwort, 422 bei falschem aktuellem PW) → optional DELETE /account/sessions (nach Passwortwechsel andere Geräte abmelden). Single-Resource (kein Bulk, kein Polling). Für Benachrichtigungen: GET /account/notification-preferences (volle Matrix lesen) → PUT /account/notification-preferences (nur die abzuschaltenden optionalen Kategorien als false senden; Default-an muss nicht mitgeschickt werden) → erneutes GET bestätigt den Round-trip.
Anti-Patterns: Kein Versuch, 2FA/E-Mail/Löschung per API zu fahren — diese Endpoints existieren bewusst nicht (siehe Sicherheits-Ausnahme). Passwort-PUT nicht ohne current_password aufrufen (deterministisch 422). DELETE /sessions löscht nie fremde User-Rows. Beim Notification-PUT nicht billing/security/team abzuschalten versuchen — diese werden serverseitig ignoriert (kein 422, aber kein Effekt); locked ist read-only.
Cross-Module-Bezüge: Token-Erstellung/-Widerruf unter /settings/api-tokens (Web). Rechnungs-/Team-Verwaltung → ## 🧾 Rechnungsdaten bzw. Team-Members-API.
Admin-only. Interner Provimedia-Kaltakquise-Pool (
leads, global — keinteam_id). Serviert ausschließlich unterv1/outbound/*mitauth:sanctum+admin-Middleware (separater Route-Block, NICHT im Kunden-v1-Block). Ein normaler Kunden-Token bekommt403am Gate — die Prospect-Liste darf nie an Kunden-Tokens leaken. Kein Team-Scoping (globaler interner Pool); dasadmin-Gate ist der einzige Zugriffsschutz. Mutationen laufen überLeadWorkflowmit Agent =null(System-Caller); Aktivitäten werden mitoutbound_agent_id = NULLgeloggt.
| Method | URL | Beschreibung | Auth · Credits |
|---|---|---|---|
| GET | /outbound/leads |
Pool-Feed, filter- + paginiert. Query: q (Volltext: Firma/Kontakt/Telefon/E-Mail/Stadt), status (Lead::STATUSES), category (werbe|seo|online_marketing|geo), assigned (null=unzugewiesen | any=zugewiesen), has (email|phone), sort (recent|company|rating|callback), page, per_page (Default 25, max 100). Paginator mit total/per_page/current_page/last_page. |
admin · 0 |
| POST | /outbound/leads |
Lead anlegen (idempotent auf dedup_key = lower(email) ?: tel:+digits(phone,≥6) ?: co:+lower(company|city)). Body: {company*, contact_name?, phone?, email?, website?, city?, region?, category?, status?}. Re-Post derselben Firma aktualisiert nur die Quelldaten und lässt den Workflow-State (Status/Zuweisung/Notizen) unangetastet. Immer 201. |
admin · 0 |
| GET | /outbound/leads/{} |
Ein Lead inkl. notes_count, activities_count, callbacks_count, is_contactable. |
admin · 0 |
| PUT | /outbound/leads/{} |
Quelldaten aktualisieren. Body: {company?, contact_name?, phone?, email?, website?, city?, region?, category?, status?}. status wird über LeadWorkflow::setStatus geführt (loggt status_changed + Sperrlisten-Side-Effect bei do_not_call); dedup_key bleibt unverändert. |
admin · 0 |
| POST | /outbound/leads/{}/notes |
Notiz anhängen (Autor „API"), loggt note_added. Body: {body* (max 2000)}. 201. |
admin · 0 |
| POST | /outbound/leads/{}/callbacks |
Wiedervorlage planen. Body: {scheduled_at* (Zukunft), note? (max 1000)}. Setzt next_callback_at + Status callback (außer won/lost/do_not_call), loggt callback_scheduled. 201. |
admin · 0 |
| POST | /outbound/leads/{}/email |
Vertriebsmail async versenden (SendSalesOfferJob, Absender „Rankion-Team"). 422 ohne E-Mail, 409 wenn nicht kontaktierbar (do_not_call/Sperrliste), sonst 202. |
admin · 0 |
| GET | /outbound/leads/{}/activities |
Timeline des Leads (neueste zuerst, paginiert, per_page max 100). |
admin · 0 |
| GET | /outbound/stats |
Funnel-Kennzahlen: {total, assigned, unassigned, with_email, by_status:{status:count}, status_labels}. |
admin · 0 |
Module ohne eigene Sektion oben — hier vollständigkeitshalber dokumentiert.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /agency/approvals |
Offene Freigabe-Anfragen des Teams auflisten (Inbox) | 0 |
| POST | /agency/approvals/{}/approve |
Aktuelle Stufe einer Freigabe-Anfrage genehmigen | 0 |
| POST | /agency/approvals/{}/reject |
Lehnt Freigabe-Anfrage ab, optional mit Feedback | — |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /brand-voices |
Brand Voices des Teams auflisten | 0 |
Bugmeldungen/Verbesserungswünsche (Plan-Hub #51, Screenshot-Anhänge seit #68). Volle Doku: docs/modules/feedback.md.
Die GET-Antworten enthalten je Meldung attachments[] mit id, url, mime_type, size_bytes, original_name. Die url zeigt auf GET /feedback/attachments/{id} (Session-Route ausserhalb von /v1, nur Melder oder Admin, sonst 403) — die Bilder liegen auf der privaten Disk und haben bewusst keine /storage/-URL.
Seit Plan-Hub #82 tragen beide GET-Antworten ausserdem page_url (die Seite, von der aus gemeldet wurde — relativer Pfad ohne Host, sensible Query-Parameter wie token, signature, secret, api_key, password, key auf *** maskiert) und device mit browser, os und device; ein nicht erkannter Wert ist null und wird nie geraten. Die ROHE User-Agent-Zeichenkette steht ausschliesslich in der Admin-Antwort GET /v1/admin/feedback im Feld user_agent — sie identifiziert den Melder genauer als die zerlegte Angabe.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /feedback |
Eigene Feedback-Meldungen auflisten (paginiert, 25/Seite) | 0 |
| POST | /feedback |
Neue Meldung einreichen (message, optional category, optional attachments[] — bis zu 3 Bilder à max. 5 MB, multipart/form-data; optional page_url — max. 2048 Zeichen, nur eine Adresse dieser Anwendung, sonst 422) — löst Bestätigungsmail aus. Eine unbrauchbare Datei ⇒ 422 und es entsteht KEINE Meldung |
0 |
| GET | /admin/feedback |
Alle Meldungen auflisten, optional ?status= (Admin) |
0 |
| PATCH | /admin/feedback/{}/status |
Status ändern: offen/in_bearbeitung/erledigt (Admin) | 0 |
| POST | /admin/feedback/{}/confirm-bug |
Als Bug bestätigen — vergibt einmalig 50 Credits ans meldende Team (Admin, idempotent) | 0 |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /engines |
Komplette Engine-Capability-Matrix: Version, Signale und alle Engines | — |
| GET | /engines/signals/{} |
Signal-Detail: Evidenz-Tier plus zustimmende und ablehnende Engines | — |
| GET | /engines/{} |
Detaildaten einer einzelnen Engine aus der Capability-Matrix | — |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /exports |
Generierte Exporte des Teams auflisten (Inbox) | 0 |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET / POST | /grounding/check-validations |
Check-Validierungs-Runs auflisten bzw. neuen Run starten (202) | — |
| GET | /grounding/check-validations/{} |
Run-Detail mit Per-Check-Ergebnissen, nach Lift sortiert | — |
| POST | /grounding/signal-observations/{}/promote |
Kandidaten-Signal auf Tier A promoten (Admin-only) | — |
| POST | /grounding/signal-observations/{}/reject |
Kandidaten-Signal-Beobachtung ablehnen (Admin-only) | — |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET / POST | /incidents |
Team-Incident-Register auflisten bzw. KI-Vorfall öffentlich melden (202) | — |
| GET | /incidents/{} |
Einzelnen team-eigenen Incident-Report abrufen | — |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /locations |
Business-Standorte des Teams auflisten (Local SEO) | 0 |
| GET | /locations/{} |
Detail eines Business-Standorts (team-gescopt) | — |
Gefuehrte Touren je Modul (Plan-Hub #79). Der Stand haengt am NUTZER, nicht am
Team: zwei Kollegen im selben Team haben eigene Staende. offered_at und
seen_at sind zwei verschiedene Zeitpunkte — angeboten ist nicht gesehen.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /module-tours |
Alle Module mit Tour, deren Schritte und der eigene Stand | 0 |
| POST | /module-tours/end-run |
Ueberblicksrunde beenden: jedes aktive Modul bekommt offered_at, Status bleibt pending |
0 |
| POST | /module-tours/{}/seen |
Tour eines Moduls als durchlaufen vermerken (idempotent, setzt seen_at) |
0 |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /optimization/runs/{} |
Detail eines Optimization-Runs mit allen Iterationen und Laufstatus | — |
| POST | /optimization/runs/{}/abort |
Bricht laufenden Optimization-Run ab (Status aborted) | — |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /search-console |
Search-Console-Uploads auflisten (Leistung + Indexierung, pro Team/Projekt) | 0 |
| GET | /search-console/{} |
Search-Console-Insights eines Uploads (Chancen + Fehler, je mit KI-Erklärung & Beispiel) | 0 |