Rankion.ai API Reference

Stand: 2026-06-27 · Version: v1 · 508 Endpoints

Single Source of Truth für die komplette REST-API. Generiert aus routes/api.php + Live-Verifikation aller Endpoints. Diese Datei wird via CLAUDE.md auto-importiert und kann als Skill-Referenz genutzt werden.

Authentifizierung

# 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.

Antwort-Formate

Credit-Mechanik

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.


🎯 AI Visibility Tracking

Projekte

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, suggested_keywords, suggested_competitors, suggested_prompts, suggested_personas, classification}
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)

Freigaben (Cross-Team-Sharing)

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

Keywords / Prompts

Method URL Notes
GET /tracking-projects/{id}/keywords ?active_only=true
POST /tracking-projects/{id}/keywords Body: {keyword, language?, country?, is_active?}
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 FilterGET /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).

Prompt-Tagging (team-weite Tag-Taxonomie + Zuweisungen)

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

Cited Sources

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?}

Insights

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_idtracking_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}
PATCH /tracking-projects/{id}/competitors Body: {domains[]} (max 50) — normalisiert/dedupliziert, synct competitor_domains + TrackingCompetitor. Returnt {competitor_domains[]}. Powert das Gap-Wettbewerber-Modal

Brand-Accuracy (Faktencheck)

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=true wenn KB leer + Projekt active (Crawl läuft noch). 403 Fremd-Team. — 0
POST /tracking-projects/{id}/brand-kb/rebuild Eigen-Domain Re-Crawl + Re-Embed der Webseiten-KB (BuildBrandKbJob, ShouldBeUnique, Queue ai, Redirects DISABLED + assertSafeUrl). Hash-Dedup (UNIQUE(project, passage_hash)) macht den Re-Build idempotent. 202 + {data:{status:"queued", tracking_project_id, message}}. throttle:5,1. Kostenlos (Eigen-Domain-Read, kein Credit-Gate) — 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_defaultdaily|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

Reality Check (AI Visibility Index)

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?}

Live Prompt Check (ad-hoc Multi-LLM-Fan-out)

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. ?project_id=&status=&per_page=&page=
POST /prompt-check Neuer Run async (202). Body: {tracking_project_id, prompt (5-5000ch), platforms:["chatgpt","claude",…], with_search?:{platform:bool}, title?} 1 pro Platform×Mode
GET /prompt-check/{id} Detail: run + alle responses mit response_text, sentiment, brands_mentioned, citations
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.


📝 Articles

CRUD

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}

Aktionen

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_modeall|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}. statuspending|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_typeimage/* — 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

Versions

Method URL Notes
GET /articles/{id}/versions ?limit=50
POST /articles/{id}/versions/{vid}/restore überschreibt article.content

Freshness

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

Internal Linking

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)

Standalone

Method URL Credits
POST /generate/article 5
POST /generate/image 5

CMS-Integrations

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

🛬 Landingpages

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.
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.

🔍 SEO Suite

Rank Tracker (Phase 1)

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)

Keyword Explorer (Phase 2)

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*

Keyword Gap (Phase 3)

Method URL
GET /gap · /gap/{project} · /gap/{project}/opportunities
PATCH /gap/opportunities/{opportunity}
POST /gap/{project}/refresh

Site Audit (Phase 4)

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/issues/{issue}/generate-brief

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.

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, deep_link}}. 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.

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):

Filter 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

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
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?})

🛠 Keywords (Research & Expansion)

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
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 — 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.

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_titlescluster_rolemain|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.


✍️ Content Intelligence

Storylines

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)

Style Profiles

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 }

Knowledge Base

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}

Orphan Scans

Method URL
POST /projects/{project}/orphan-scans/discover
POST /orphan-scans/{id}/start
GET /orphan-scans/{id} · .../pages
DELETE /orphan-scans/{id}

Overview

Method URL
GET /content-intelligence
GET /content-freshness

📅 Calendar & Images

Calendar

Method URL
GET/POST /projects/{project}/calendar
PUT/DELETE /calendar/{id}

Project Images (legacy per-project listing)

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)

Dateien (Nicht-Bild-Chat-Dateien, team-scoped)

Komplement zur Image Gallery: alles mit mime_typeimage/* (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

🤖 AI Tools

Agentic Chat — Master Agent als Endpoint

Method URL Credits
POST /agentic/chat varies (depends on tools used by the agent)

Body:

Query-Parameter:

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:

AI Scanner / Detection

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.

Humanizer

Method URL Credits
POST /humanize 8
GET /humanize/{batch} · .../documents/{document}

Content Optimizer

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.


🥊 Competitor Analysis

Method URL Credits
GET/POST /competitor-analyses 20 (POST)
GET /competitor-analyses/{id} · .../gaps (?is_favorite=&is_dismissed=)
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

📊 Content Audits (Phase 4 — site-wide content scanner)

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:

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:

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:

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.


👥 Community Monitor

Method URL Credits
GET /community/mentions · /community/mentions/{id}
POST /community/scan Body: {keyword, platforms[]} 5
GET /community/alerts

📈 Site Monitor

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

🚀 Automation

Bulk Generations

Method URL Credits
GET/POST /bulk-generations Body: {project_id, keywords[], type?, length?} 15 (POST)
GET /bulk-generations/{id}

Autopilot

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.

Pipelines

Method URL Credits
GET/POST /pipelines Body: {name, stages[]} 20 (POST)
GET /pipelines/{id}
POST /pipelines/{id}/retry

🏗 Projects & Settings

Method URL
GET/POST /projects
GET/PUT/DELETE /projects/{id}
GET/POST /goals · DELETE /goals/{id}
GET /modules
POST /modules/{module}/activate · .../deactivate

🎯 Action Center

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

🔌 Google Integrations

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

⭐ Review Sources

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)

📰 Blog (Admin)

Method URL
GET/POST /blog/categories
PUT/DELETE /blog/categories/{id}
GET/POST /blog/posts
GET/PUT/DELETE /blog/posts/{id}

💾 Rankion OS

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

💳 Account / Meta

Method URL
GET /team
GET /credits · /credits/history
GET /dashboard
GET /admin/api-costs (Admin only)

🧾 Rechnungsdaten

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?}

Workflow (GEO-Optimizer-Perspektive)

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.


👥 Team-Mitglieder

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

Workflow (GEO-Optimizer-Perspektive)

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.


🔬 Page-Deep-Audit (Vision + KI-Render)

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: pendingscrapingscreenshottinganalyzingcompleted. 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:


📑 Reports & Cross-Module Correlation

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.

Reports

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: pendinggeneratingcompleted (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).

Cross-Module Correlation

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).


Beispiele (curl)

Wizard-Flow komplett über API

# 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
  }'

Keyword Explorer mit allen Daten

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

Article generieren + publishen

# 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}'

Cited-Sources Outreach

# 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

Konventionen

Routes-Generierung

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


Chat Shares

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

POST /v1/chat-shares

Body:

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": "..."
  }
}

PATCH /v1/chat-shares/{id}

Same fields as POST plus freeze: bool (true = freeze content snapshot, false = back to live).

DELETE /v1/chat-shares/{id}

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).

Public route (NOT exposed via API)

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.


Agentic Plans

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

Plan response shape

{
  "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"
  }
}

Step response shape

{
  "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).

📜 OpenAPI 3.1 Spec + Error-Code-Vokabular (Subsystem D)

Maschinenlesbarer Vertrag für Subsystem A + C — fixed 2026-05-09.

GET /api/v1/openapi.yaml

OpenAPI 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:

Andere Endpoints sind weiterhin nur in dieser Markdown-Reference dokumentiert; eine vollständige OpenAPI-Export-Phase ist auf der Roadmap.

Error-Code-Vokabular (kanonisch)

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

Rate-Limit-Headers

Jeder Endpoint mit throttle:N,M Middleware liefert auf erfolgreiche Responses:

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:


🤖 Master-Agent Härtung (Subsystem C)

Drei Härtungen am /api/v1/agentic/chat Flow für robustere programmatische Caller — fixed 2026-05-09.

P1.4 — Strukturierter context_overflow Error

Wenn 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".

P1.5 — tool_policy im Request

Restriktiere 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.

P1.6 — Async-Default + Polling-Endpoint

/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).

Rückfragen / Bestätigungen über die API (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.


🧭 Grounding Audit Pipeline (Subsystem A)

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}. 404 (audit_not_found), 410 (audit_expired). 0
GET /v1/grounding/audits Liste eigener Audits. 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

Tier-Schwellen

Score Tier
0–25 not-grounded
26–50 weak
51–75 partial
76–100 grounded

Findings-Schema

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.

Webhook-Verifikation

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'));

Error-Codes (Subsystem A)

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.

🧬 GEO Criteria Catalog

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):

Off-Page-Readiness (Demand-Achse)

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 }
}

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.

📚 Wiki

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?".

🧠 Deep Insights (LLM Data Mining)

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.

🧬 Citation Causality (CCE)

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):

  1. Trigger: POST /admin/sandbox/runs mit citation_filter:{project_ids:[N], limit:30} + serp_counterfactual_enabled:true
  2. Polling: GET /admin/sandbox/runs/{runId} alle 30s bis status: completed (Run dauert 5-15 min)
  3. Iteration: Bei unzufriedenstellenden Patterns: Catalog-Editor unter /admin/citation-causality-lab/catalog für Klon + Anpassung, dann neuer Run mit dimensions_catalog_id: <neue_id>
  4. Bulk-vs-Single: Tier-1 Per-Page-Audit (1 URL on-demand) vs Tier-3 Globale Pattern-Pipeline (alle Citations)
  5. Abschluss: Lift-Scores aus 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):

  1. Recipe abrufen: GET /projects/{id}/recipe — Cache-Hit liefert sofort, sonst <30s glm-4.7 Call
  2. Lever-Übersicht: GET /projects/{id}/levers für Top-Buckets, optional ?include=trend,by_llm für detaillierte Inspection
  3. Drill-Down: GET /projects/{id}/levers/{key}/{value}/drill-down für Evidence (cited URLs + SERP-Counterfactuals)
  4. Audit dispatchen: POST /projects/{id}/audit (5 Credits) → 202 + Job-Marker; Status via existierenden /audit/bulk/{auditId} Endpoint pollen
  5. Anti-Patterns: Recipe vor genug Sample-Data → leerer Output (sample-floor returnt available:false); Drill-Down auf bad-format Key → 404

Live-Dashboards:


⚡ Prefetch (Rankion Turbo)

Team-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.


🔐 Account / Sicherheit

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).

Sicherheits-Ausnahme — bewusst NICHT per API exponiert

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).

Workflow (GEO-Optimizer-Perspektive)

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.