Raw Markdown · rankion-api-reference.md
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.
# Token im Frontend erstellen: /settings/api-tokens
TOKEN="<dein-sanctum-token>"
BASE="https://rankion.ai/api/v1"
curl -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
"$BASE/projects"
Alle Endpoints sind Team-scoped via auth:sanctum Middleware. Cross-Team-Zugriff → 403.
keyword_id, article_id etc. zum Pollen.{message, errors:{field:[reason]}}Jobs/Aktionen mit credits:N Middleware ziehen N Credits aus dem Team-Balance vor Job-Dispatch. Bei cache=false Refresh (Explorer) wird inline 50 Credits abgebucht.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /tracking-projects |
alle Team-Projekte | — |
| POST | /tracking-projects/analyze |
Wizard Step 1+2: Domain/Keyword analysieren, dispatcht AnalyzeDomainJob. Body: {mode:"domain"|"keyword", domain?, keyword?, name?, language?, country?, own_domains[]?} → 202 + project_id |
— |
| POST | /ai-visibility/ingest |
Guided Prompt-Ingest (skill/agent/MCP front door). Schickt eine Prompt-Liste; State-Machine status: needs_input (fragt domain+market) → analyzing → needs_confirmation → activated. Body: {prompts[], domain?/brand?, language?, country?, project_id?, confirm?, auto_confirm?, platforms?, frequency?, start_run?}. Orchestriert analyze→activate→classify; team-scoped, idempotent (team,domain). Doku: docs/modules/ai-visibility-ingest.md |
analyze 5 / activate 0 |
| GET | /ai-visibility/ingest/{id} |
Poll des Ingest-/Analyse-Status (gleiche Envelopes). Team-scoped (403 cross-team) | — |
| GET | /tracking-projects/{id} |
Detail mit KPIs | — |
| PUT | /tracking-projects/{id} |
Settings: {name?, brand_name?, brand_aliases[], competitor_domains[], own_domains[], tracking_frequency?, llm_platforms[], llm_with_search?, llm_without_search?, track_aio?, reality_check_enabled?} — llm_platforms[] erlaubt: chatgpt, perplexity, claude, gemini, copilot, grok, google_ai_mode — own_domains = weitere eigene Domains (Full-Replace, normalisiert, schließt Hauptdomain aus, reklassifiziert Cited Sources sofort) |
— |
| DELETE | /tracking-projects/{id} |
⚠ Projekt samt aller Daten löschen (Keywords, Prompts, Runs, Scores, Cited-Sources, LLM-Results + alle CASCADE-Children). Irreversibel. Team-gescoped (403 cross-team). → 204 No Content | nur eigenes Team · 0 |
| GET | /tracking-projects/{id}/analysis-status |
Wizard polling: {analysis_status, analysis_phase, 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) | — |
Ein TrackingProject an einen anderen bestehenden Rankion-Nutzer (auch in einem anderen Team) mit Lese- UND Schreibzugriff freigeben. Läufe, die ein Gast startet, belasten das Eigentümer-Team. Auth ist pro Endpoint asymmetrisch (Eigentümer vs. Eingeladener) — siehe „Auth"-Spalte.
| Method | URL | Beschreibung | Auth · Credits |
|---|---|---|---|
| GET | /tracking-projects/{id}/shares |
Freigaben eines Tracking-Projekts auflisten (Status, eingeladene E-Mail, Berechtigung) | nur Eigentümer · 0 |
| POST | /tracking-projects/{id}/shares |
Freigabe anlegen (Einladung). Body: {email} — lädt einen bestehenden Nutzer mit permission:"write" ein. Idempotent (reaktiviert abgelaufene/terminierte Freigaben auf derselben Zeile). Stellt via „Mit mir geteilt" + E-Mail (TrackingProjectShared) zu |
nur Eigentümer · 0 |
| DELETE | /tracking-projects/{id}/shares/{share} |
Freigabe widerrufen (status→revoked) |
nur Eigentümer · 0 |
| GET | /shared-with-me |
Mit dem authentifizierten Nutzer geteilte Projekte (Gast-Sicht) | jeder · 0 |
| POST | /tracking-project-shares/{share}/accept |
Freigabe annehmen (E-Mail-Match, atomarer lockForUpdate-Consume → status→active) |
nur Eingeladener · 0 |
| POST | /tracking-project-shares/{share}/decline |
Freigabe ablehnen (status→declined) |
nur Eingeladener · 0 |
| DELETE | /tracking-project-shares/{share} |
Freigabe entkoppeln/verlassen (status→detached) |
nur Eingeladener · 0 |
| Method | URL | Notes |
|---|---|---|
| GET | /tracking-projects/{id}/keywords |
?active_only=true |
| POST | /tracking-projects/{id}/keywords |
Body: {keyword, 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 Filter — GET /tracking-projects/{id}/prompts akzeptiert: ?search= (Volltextsuche auf prompt_text, MariaDB-FULLTEXT Boolean-Mode: mehrere Wörter = UND, +pflicht -ausschluss "exakte phrase"; Substring-Fallback bei <3-Zeichen-Termen), ?tags[]= (Tag-IDs; OR innerhalb Dimension, AND zwischen Dimensionen), ?category[]= (string ODER array, back-compat), ?intent[]=, ?funnel[]=, ?persona[]=, ?language[]=, ?monitor_status[]=, ?volume_min=, ?active_only=1, ?per_page= (paginiert sonst flache Liste).
Alle Ressourcen team-gescoped (Fremd-Team → 404). 4 System-Dimensionen (topic/competitor/sub_intent/campaign) pro Team auto-geseedet.
| Method | URL | Notes |
|---|---|---|
| GET | /tag-dimensions |
Team-Dimensionen (+tags_count) · 0 |
| POST | /tag-dimensions |
{key:/^[a-z0-9_]+$/, label, color?, icon?} → 201 · 0 |
| PATCH | /tag-dimensions/{id} |
{label?, color?, icon?, sort_order?} · 0 |
| DELETE | /tag-dimensions/{id} |
System-Dimension → 422 · 0 |
| GET | /prompt-tags |
?dimension={id} optional · 0 |
| POST | /prompt-tags |
{dimension_id, label, color?} — slug-dedupe → 201 · 0 |
| PATCH | /prompt-tags/{id} |
{label?, color?, description?} (label → neuer slug) · 0 |
| DELETE | /prompt-tags/{id} |
aktive Assignments vorhanden → 422 (erst mergen/lösen). Soft-Delete · 0 |
| POST | /prompt-tags/{id}/merge |
{target_id} — Assignments umhängen+dedupen, Quelle soft-delete · 0 |
| POST | /tracking-prompts/{id}/tags |
{tag_id} zuweisen → 201 · 0 |
| DELETE | /tracking-prompts/{id}/tags/{tagId} |
Zuweisung entfernen · 0 |
| POST | /tracking-projects/{id}/prompts/bulk-tag |
{prompt_ids[≤500], tag_id, operation:add|remove} → {updated, skipped} · 0 |
| Method | URL | Notes |
|---|---|---|
| GET | /tracking-projects/{id}/cited-sources |
?domain=&outreach_status=&is_own_domain=&sort=&per_page=. Zeile enthält in_portfolio (bool, „Anfrage möglich"), citation_possibility_score (int, persistierter Floor 95 für Portfolio-Domains) + effective_possibility (int, identischer Read-Time-Wert) |
| GET | /tracking-projects/{id}/cited-sources/export |
CSV-Stream. ?from=&to=&status=&platform=&search=&sort= |
| POST | /tracking-projects/{id}/cited-sources/analyze |
Citation-Analyse dispatchen. Body optional {source_ids:[]}. Default: alle unanalyzed. → 202 + {queued, estimated_seconds} |
| PATCH | /tracking-projects/{id}/cited-sources/{source_id} |
Body: {outreach_status?:neu|anfrage_gestellt|verlinkt|abgelehnt|ignoriert, notes?} |
| Method | URL | Notes |
|---|---|---|
| GET | /tracking-projects/{id}/scores |
Score-Historie (?from=&to=) |
| GET | /tracking-projects/{id}/platform-breakdown |
(?from=&to=) |
| GET | /tracking-projects/{id}/insights/low-hanging-fruit |
(?days=30&min_possibility=60&limit=5) |
| GET | /tracking-projects/{id}/insights/platform-gap |
(?days=30) |
| POST | /tracking-projects/{id}/insights/platform-gap/{platform}/diagnose |
async — 1 Credit |
| GET | /tracking-projects/{id}/insights/potential-hero |
{eligible:false} wenn <20 Abfragen |
| GET | /tracking-projects/{id}/off-page-readiness |
Off-Page-/Demand-Achsen-Score — 0 Credits, team-scoped. Returns {score:int|null, data_available, components:{geo.offpage.brand_demand, geo.offpage.third_party_mentions, geo.offpage.source_affinity → {score, data_available, meta, weight}}, meta}. score=null=noch keine Daten (≠0). 403 Fremd-Team, 404 unbekannt. SSOT OffPageReadinessScorer (bridged community_project_id ↔ tracking_project_id). Modul: geo-criteria-catalog |
| POST | /tracking-projects/{id}/prompts/{prompt}/generate-brief |
Content-Brief via Claude — 3 Credits |
| POST | /tracking-projects/{id}/brand-aliases |
Body: {alias} |
| PATCH | /tracking-projects/{id}/competitors |
Body: {domains[]} (max 50) — normalisiert/dedupliziert, synct competitor_domains + TrackingCompetitor. Returnt {competitor_domains[]}. Powert das Gap-Wettbewerber-Modal |
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_default ∈ daily|weekly|biweekly|monthly|off; override = der gesetzte Wert oder null (= erbt). last_crawled_at = jüngster brand_kb_passages.crawled_at. team-/share-scoped (authorizeTrackingProject → 403 Fremd-Team) — 0 |
| PATCH | /tracking-projects/{id}/kb-refresh |
Setzt die Pro-Projekt-Häufigkeit. Body: {frequency: daily|weekly|biweekly|monthly|off|null} (present+nullable) — null löscht den Override (erbt Team-/Config-Default). Validiert gegen BrandKbRefreshFrequency::values() → 422 bei ungültigem Wert. Returnt das identische GET-Payload (kbRefreshPayload). team-/share-scoped. throttle:5,1 — 0 |
| GET | /team/kb-refresh-default |
Team-Standard-Häufigkeit für die Auto-KB-Auffrischung. {data:{frequency: daily|weekly|biweekly|monthly|off}} — Team::getBrandKbRefreshDefault(); fällt auf config('brand_accuracy.kb_refresh_default') (weekly) zurück, wenn keine Settings-Row existiert. Scope: currentTeam() — 0 |
| PATCH | /team/kb-refresh-default |
Setzt den Team-Standard. Body: {frequency: daily|weekly|biweekly|monthly|off} (required, kein null). Validiert gegen BrandKbRefreshFrequency::values() → 422 sonst; Team::setBrandKbRefreshDefault() (Settings-Row). Returnt {data:{frequency}}. Scope: currentTeam(). throttle:5,1 — 0 |
| GET | /brand-knowledge |
Chat-Erdung der eigenen Marke: team-scoped Faktencheck-Paket in EINEM read-only Call. Auflösung der Marke via ?brand= (id/name/domain) bzw. Auto-Single, sonst disambiguation. Query: ?brand=&q=&top_k=&include=. q = KB-Query (RAG, top_k ∈ 1..10, Default 5); include = CSV-Subset aus kb,facts,truth (Default alle). FLAT-Shape: {brand{id,name,domain}, armed, kb_status, kb_passages[], fact_errors[], truth_sources[], disambiguation[]} (kb_passages mit source_url-Beleg) bzw. {brand:null, disambiguation[], message}. Liest BrandKnowledgeService (KB-Passagen + Claim-Verdicts + kuratierte Truth-Sources), kein Re-Crawl (dafür brand-kb/rebuild). Kuratierter Skill ai-visibility.brand-knowledge. auth:sanctum, throttle:60,1. 403 Fremd-Team. — 0 |
| Method | URL | Notes |
|---|---|---|
| GET | /tracking-projects/{id}/avi |
neuester AVI-Score, tier (red/yellow/green), 6-Dim-Scores, recommendations. 404 wenn kein Run |
| GET | /tracking-projects/{id}/dimensions |
alle 6 Dimension-Scores pro Plattform |
| POST | /tracking-projects/{id}/reality-check-report |
PDF-Generierung async. 404 wenn kein Run |
| GET | /tracking-projects/{id}/reality-check-report/status |
Polling-Endpoint für die laufende Report-Generierung: {status: queued|running|ready|failed, progress?, download_url?} |
User-Prompt geht parallel an ausgewählte LLMs (ChatGPT, Perplexity, Claude, Gemini, Copilot, Grok) je mit/ohne Web-Search. Live-Card-Grid in der UI, persistente Runs, Sentiment + Brand-Extract pro Antwort.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /prompt-check |
Liste runs des Teams. ?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.
| 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} |
| Method | URL | Credits |
|---|---|---|
| POST | /articles/{id}/score |
— |
| POST | /articles/{id}/optimize |
— |
| POST | /articles/{id}/generate — Body: {keyword, article_type?, outline?, target_length?, tone?, language?, style_profile_id?, content_goal_id?, use_knowledge_base?, knowledge_mode?, knowledge_document_ids?}. knowledge_mode ∈ all|specific; bei specific werden knowledge_document_ids[] strikt auf die ready-Docs des Projekts gescopt (fremde IDs verworfen) |
5 |
| POST | /articles/{id}/publish — Body: {cms_integration_id}. Unterstützt WordPress und Shopify (canonical job-mapping). 202 mit {publication_id, status:pending}; 409 wenn bereits in flight; 422 bei nicht-unterstütztem CMS-Typ |
— |
| GET | /articles/{id}/publications — CMS-Veröffentlichungs-Historie des Artikels (neueste zuerst). data[]: {id, cms_integration_id, cms_name, cms_type, status, external_id, external_url, error_message, published_at, created_at}. status ∈ pending|publishing|published|failed |
— |
| GET | /articles/{id}/images — Artikelbilder (Beitragsbild zuerst) |
— |
| POST | /articles/{id}/images — KI-Bild generieren. Body: {prompt, model?, style?}. model ∈ dall-e-2/3, gpt-image-1/2, flux-schnell/dev, flux-1.1-pro. 202 mit image_id; 402 bei zu wenig Credits |
3–10 (je Modell) |
| POST | /articles/{id}/images/from-gallery — bestehendes Team-Galerie-Bild als Beitragsbild setzen. Body: {file_id} (AgenticUploadedFile-ID). Kopiert Datei auf public Disk, legt Image an, setzt exklusiv is_featured. 201 mit {image_id, url, is_featured}; 404 wenn File nicht im Team oder kein Bild (mime_type≠image/* — die Galerie zeigt nur Bilder, agent-generierte Dokumente wie xlsx/csv/md sind ausgeschlossen); 422 wenn Quelle weg |
— |
| PATCH | /images/{id}/featured — Bild als Beitragsbild markieren (exklusiv pro Artikel) |
— |
| POST | /articles/{id}/repurpose — Body optional {format:linkedin|twitter|instagram|newsletter|youtube_script|tiktok_script|facebook}. Ohne format → alle |
3 |
| Method | URL | Notes |
|---|---|---|
| GET | /articles/{id}/versions |
?limit=50 |
| POST | /articles/{id}/versions/{vid}/restore |
überschreibt article.content |
| Method | URL |
|---|---|
| GET | /articles/{id}/freshness |
| POST | /articles/{id}/freshness/check (read-only score, 0 credits — non-destructive) |
| POST | /articles/{id}/freshness/refresh (AI-rewrite → DRAFT + version backup, contentRefresh credits — never auto-publishes) |
| GET | /articles/{id}/freshness/history |
| Method | URL |
|---|---|
| GET | /articles/{id}/link-suggestions |
| POST | /projects/{project}/internal-links/analyze (async, 202 + run_id; 1 Credit/Artikel im Job — kein flat charge) |
| GET | /internal-links/runs/{run} (Run-Status/Progress pollen) |
| PUT | /link-suggestions/{id} (status=applied wendet den Link an + erreicht das Live-CMS; draft-applied wenn Quelle unveröffentlicht) |
| Method | URL | Credits |
|---|---|---|
| POST | /generate/article |
5 |
| POST | /generate/image |
5 |
| Method | URL | Credits |
|---|---|---|
| POST | /cms-integrations/{id}/test-connection — prüft die hinterlegten Credentials live (WordPress /users/me, Shopify /shop.json). Reiner Probe-Call (keine Persistenz, idempotent), Throttle 20/min. 200 mit {data:{ok:bool, detail}}; 404 fremdes Team; 422 nicht-unterstützter CMS-Typ |
— |
Phase-1-REST-Slice: Polling einer laufenden Bulk-Generierung (read-only, team-gescopt, 0 Credits). Idiom/Anti-Patterns: siehe
docs/modules/landingpage-generator.md→ „Workflow (GEO-Optimizer-Perspektive)".
| Method | URL |
|---|---|
| GET | /landingpages/{designSystem}/pages (?status=&page=&per_page= ≤100) — paginierte Seiten mit progress{state,percent,eta_seconds,elapsed_seconds,expected_seconds,started_at} + pro Seite cta{url,status,type,suggested_label,buttons} (buttons=Per-Button-Override oder null) + batch{total,done,in_flight,percent} + expected_seconds. Fremde/unbekannte designSystem → 404. |
| PATCH | /landingpages/{designSystem}/cta Body {page_ids[],cta_url} — optionale CTA-Ziel-URL (1+ Seiten) zuweisen + async scrapen/KI-klassifizieren (202). Team-gescopt (ids auf DS+Team re-skopt), 0 Credits. |
| PATCH | /landingpages/pages/{landingPage}/cta-buttons Body {buttons:[{section,label,url}]|null} — Per-Button-CTA-Override EINER Seite (null=AUTO, []=keine, sonst genau diese, je url:http,https, ≤4). Synchron re-gerendert; Response {cta_buttons,effective[],rerendered}. Team-gescopt, fremde Seite → 404. 0 Credits. |
| Method | URL |
|---|---|
| GET | /rank-tracker |
| GET | /rank-tracker/{project} |
| GET | /rank-tracker/{project}/keywords (?device=&country=&is_active=) |
| POST | /rank-tracker/{project}/keywords Body: {keywords[], devices[]} async 202 |
| POST | /rank-tracker/{project}/refresh |
| GET | /rank-tracker/keywords/{keyword}/history (30d/90d/365d) |
| Method | URL | Credits |
|---|---|---|
| POST | /explorer Body: {seed, language?, country?, cache?}. cache=false umgeht 60-min-Cache |
50 |
| GET | /explorer/searches Such-Historie des Teams (paginiert, ?page=&per_page= max 100). Items: {id, keyword, language, country, created_at, searched_at} |
0 |
| POST | /explorer/searches/{id}/refresh Refresh eines History-Eintrags: frischer Pull (Cache-Bust), 202 + {keyword, language, country}. Abrechnung bei Job-Erfolg |
50 |
| DELETE | /explorer/searches/{id} History-Eintrag löschen (204; gecachte Ergebnisse bleiben). 404 bei fremdem Team |
0 |
| GET | /explorer/{keyword} Volume + KD + Trends + SERP. ?include=related,questions inlined die Listen. ?cache=false erzwingt fresh fetch |
0 / 50* |
| GET | /explorer/{keyword}/related ?type=related|question. ?cache=false |
0 / 50* |
| Method | URL |
|---|---|
| GET | /gap · /gap/{project} · /gap/{project}/opportunities |
| PATCH | /gap/opportunities/{opportunity} |
| POST | /gap/{project}/refresh |
| Method | URL |
|---|---|
| GET/POST | /site-audit |
| GET | /site-audit/{crawl} · .../issues |
| PATCH | /site-audit/{crawl}/issues/bulk |
| PATCH | /site-audit/issues/{issue} |
| POST | /site-audit/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):
grounding_v1_5_gp_h1_entity_only — H1 not pure entity namegrounding_v1_5_gp_lead_single_sentence_definition — lead is not a 1-sentence definitiongrounding_v1_5_gp_lead_retrieval_sentence — missing retrieval-stabilization sentencegrounding_v1_5_gp_h2_entity_prefix, grounding_v1_5_gp_disambiguation_blockgrounding_v1_5_gp_human_notice, grounding_v1_5_gp_timestamps_visiblegrounding_v1_5_gp_anchors_stable_ids, grounding_v1_5_gp_facts_dl_gridgrounding_v1_5_gp_jsonld_present, grounding_v1_5_gp_jsonld_faq_when_html_faqgrounding_v1_5_gp_head_canonical_absolute, grounding_v1_5_gp_eeat_author_bio_schemagrounding_v1_5_gp_faq_entity_repeated, grounding_v1_5_gp_volatile_dated_sourcedFilter via GET .../issues?issue_type=grounding_v1_5_gp_h1_entity_only. Bulk-mark via filter:{issue_type:"grounding_v1_5_gp_eeat_author_bio_schema"}.
| Method | URL | Beschreibung |
|---|---|---|
| GET | /backlinks · /backlinks/{project} · .../domains · .../anchors · .../events |
Phase 5 base |
| POST | /backlinks |
Activate-Endpoint: Backlink-Tracking für Projekt aktivieren (Subscription required) |
| POST | /backlinks/{project}/refresh |
Trigger Delta-Pull Job |
| POST | /backlinks/{project}/full-refresh |
Vollständigen DFS-Re-Pull triggern (Async 202) |
| GET | /backlinks/{project}/spam-scores |
Toxische Domains (Filter ?min_spam=60) |
| POST | /backlinks/{project}/spam-scores/refresh |
Queue EnrichSpamScoresJob (202) |
| GET | /backlinks/{project}/disavow-export |
text/plain Disavow.txt (Param ?threshold=80) |
| GET | /backlinks/{project}/timeseries |
Historische Velocity-Punkte (?granularity=daily|weekly|monthly) |
| POST | /backlinks/{project}/anchors/refresh |
Queue PullDetailedAnchorsJob (202) |
| GET | /backlinks/{project}/links |
TODO-050 Per-Link-Browser: einzelne Backlinks (source_url→target_url, anchor, follow). Filter ?sort=last_seen|first_seen|source_trust_flow|anchor_text, ?follow=dofollow|nofollow, ?lost=1. Credits 0 |
| GET | /backlinks/{project}/lost-links |
TODO-050 Verlorene/gelöschte Einzel-Links (is_deleted OR lost_at), sortiert nach lost_at desc. Credits 0 |
| GET | /backlinks/{project}/disavow-recommendations |
TODO-050 Top toxic_severity=high Referring-Domains + Gründe (Disavow-Empfehlung; vom Action-Center/Agent zitierbar). Credits 0 |
| POST | /backlinks/{project}/link-gap |
TODO-050 Competitor-Link-Gap starten (Async 202). Body {competitor_domains?:[]} (Default = TrackingProject competitor_domains). Credits 10 (backlink_link_gap) |
| GET | /backlinks/{project}/link-gap |
TODO-050 Letzten Link-Gap-Lauf lesen (processing_status → results[]). Credits 0 |
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?}) |
| 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_titles — cluster_role ∈ main|sub|solo, jede Zeile trägt
cluster_id + cluster_main_keyword (selbsterklärend für LLM-Weiterverarbeitung).
Sortierung: größte Cluster zuerst, im Cluster main → subs nach Volumen.
GET /projects/{project}/keyword-researches — Recherche-Historie (Wurzeln mit
source, processing_status, keywords_count), paginiert.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /projects/{project}/storylines |
Liste aller Storylines (paginiert, 25/Seite) | 0 |
| POST | /projects/{project}/storylines |
TODO-047 async Outline-Generator: keyword (req), target_word_count?, goal_id?, language?, use_knowledge_base?/knowledge_mode?/knowledge_document_ids?[]. Erstellt generating-Platzhalter, dispatcht GenerateStorylineApiJob, liefert 202 {storyline_id,status:'generating'}. Job zieht die Credits einmal bei Erfolg. Legacy: Payload mit structure[] → synchroner Store (201, keine Generierung, keine Credits). |
5 |
| GET/PUT/DELETE | /storylines/{id} |
Detail (inkl. research_data — SERP heading-gap/PAA/featured snippet), Update (Status-Enum inkl. failed), Löschen |
0 |
| POST | /storylines/{id}/approve |
Review-Gate vor Artikel-Assembly. Setzt status=approved+approved_at+approved_by; idempotent |
0 |
| POST | /storylines/{id}/assemble-article |
Unified Storyline→Artikel (gleicher Pfad wie der In-App-Button): baut Article mit Projekt-Sprache + style_profile_id?/tone?, verknüpft articles.storyline_id+storyline.article_id, dispatcht GenerateArticleJob, liefert 202 {article_id}. 422 wenn nicht approved/completed. Single-use (2. Aufruf → bestehender Artikel) |
10 (= FeatureCost::article) |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /projects/{project}/style-profiles |
Auflisten (Response enthält zusätzlich source_type, source_url, pending_review, scraped_pages, failure_reason, reviewed_at) |
— |
| POST | /projects/{project}/style-profiles |
Manuell erstellen | — |
| GET | /style-profiles/{id} |
Detail (gleiche Zusatzfelder) | — |
| PUT | /style-profiles/{id} |
Aktualisieren. Wenn pending_review=true, wird das Flag implizit auf false gesetzt und reviewed_at=now() belegt (Detail-Edit zählt als Review-Akzeptanz). |
— |
| DELETE | /style-profiles/{id} |
Löschen | — |
| POST | /projects/{project}/style-profiles/from-url |
URL-basierte Stilanalyse als Background-Job starten. ScraperAPI lädt bis zu 10 inhaltsstarke Seiten, Claude erstellt das Profil. Fertiges Profil hat pending_review=true. HTTP 202 → { data: { id, status: "pending", source_url, message } } |
25 |
| GET | /style-profiles/{id}/scrape-status |
Polling-Endpoint für laufenden Background-Job. Liefert { status, source_url, scraped_pages, failure_reason, pending_review } |
— |
| POST | /style-profiles/{id}/accept |
Profil akzeptieren: setzt pending_review=false, reviewed_at=now(). Response: { data: <StyleProfile> } |
— |
| POST | /style-profiles/{id}/discard |
Profil verwerfen (Soft-Delete). Nur wenn pending_review=true, sonst 422. Response: { deleted: true } |
— |
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /projects/{project}/knowledge-base |
Docs + Status auflisten (Polling) | — |
| POST | /projects/{project}/knowledge-base — Body: {title, content, type?} |
Rohtext ingestieren: persistiert, chunked + embedded async, gibt 202 {document_id, status:"processing"} zurück (kein 201/active mehr) |
5 |
| GET | /projects/{project}/knowledge-base/search — Query: {q, top_k?} |
Hybride Suche (Ollama-Embedding-Cosine ∪ Keyword, dann Haiku-Rerank) über ready-Chunks; {data:[{id,document_id,snippet,score}], meta} |
1 |
| DELETE | /knowledge-base/{id} |
Doc löschen (Cascade-Delete der Chunks) | — |
| GET | /knowledge-base/{id}/download |
Datei herunterladen (Original-Upload bzw. extrahiertes/synthetisiertes Markdown) | — |
| POST | /knowledge-base/url |
URL ingestieren (async) | 5 |
| POST | /projects/{project}/knowledge-base/site-analysis — Body: {url, max_pages?} (20–80, Default 40) |
Website-Analyse starten: agentische Pipeline crawlt die Domain, klassifiziert per LLM und synthetisiert bis zu 9 KB-Dokumente (source_type=site_analysis, Re-Analyse ersetzt alte Docs). 202 {analysis_id, status, poll_url}; 409 = Run für Host aktiv, 422 = SSRF |
25 |
| GET | /projects/{project}/knowledge-base/site-analysis |
Analyse-Runs eines Projekts (paginiert, mit progress_percent) |
— |
| GET | /knowledge-base/site-analysis/{id} |
Run-Status pollen: status (pending→discovering→extracting→synthesizing→completed|partial|failed), progress_percent, Seiten-Counter, primary_language, document_ids |
— |
| Method | URL |
|---|---|
| GET/POST | /projects/{project}/link-lists |
| DELETE | /link-lists/{id} |
| Method | URL |
|---|---|
| POST | /projects/{project}/orphan-scans/discover |
| POST | /orphan-scans/{id}/start |
| GET | /orphan-scans/{id} · .../pages |
| DELETE | /orphan-scans/{id} |
| Method | URL |
|---|---|
| GET | /content-intelligence |
| GET | /content-freshness |
| Method | URL |
|---|---|
| GET/POST | /projects/{project}/calendar |
| PUT/DELETE | /calendar/{id} |
| Method | URL | Beschreibung |
|---|---|---|
| GET | /projects/{project}/images |
Listet alle Bilder eines Projekts (legacy — die team-weite Gallery /v1/images unten ist der primäre Pfad) |
Cross-Team-IDs liefern 404 (nicht 403, kein Existenz-Leak).
| Method | URL | Credits | Beschreibung |
|---|---|---|---|
| GET | /images |
0 | Liste mit Filter: ?q, ?model, ?from, ?to, ?favorite, ?tags[], ?sort=newest|oldest|edited|name|model|size|date, ?dir=asc|desc (nur für name/model/size/date — spiegelt die Spalten-Sortierung der Galerie-Tabellenansicht), ?page, ?per_page |
| GET | /images/trash |
0 | Soft-deleted Bilder |
| GET | /images/{id} |
0 | Detail inkl. variants[] und parent-Tree |
| POST | /images/{id}/edit |
10 | Edit-Variante via OpenAI Images-Edit-API (openai:/images/edits, extern). Async 202, Body: {job_id, edit_session_id}. Frontend pollt /images/{id} |
| DELETE | /images/{id} |
0 | Soft-delete (30 Tage Trash, dann hard-delete) |
| POST | /images/{id}/restore |
0 | Wiederherstellen aus Trash |
| DELETE | /images/{id}/forever |
0 | Endgültig löschen (NUR aus Trash, sonst 409) |
| POST | /images/{id}/favorite |
0 | Toggle is_favorite |
| PATCH | /images/{id} |
0 | Update Metadaten: {title?, tags?[]} |
| POST | /images/bulk/delete |
0 | Body {ids:[]} max 200 |
| POST | /images/bulk/restore |
0 | Body {ids:[]} max 200 |
| POST | /images/bulk/export |
0 | Body {ids:[]} max 500. Async 202 → {job_id} für ZIP |
| GET | /images/exports/{job_id} |
0 | Export-Status: {status: queued|ready|failed, download_url?} |
| POST | /images/{id}/open-in-chat |
0 | Erstellt AiChatSession mit gallery_image_id={id}. Returns {session_id} für Redirect zu /chat/{session_id} |
| POST | /images/{id}/share |
0 | Idempotenter Public-Share-Token. Body: {expires_at?}. Returns {token, public_url, expires_at, view_count, is_active} |
| PATCH | /images/{id}/share |
0 | Expiry ändern. Body: {expires_at: ISO|null} (404 wenn kein aktiver Share) |
| DELETE | /images/{id}/share |
0 | Public-Share widerrufen (View-Count bleibt erhalten) |
Komplement zur Image Gallery: alles mit mime_type ≠ image/* (agent-generierte xlsx/csv/md + User-Uploads). Cross-Team- ODER Bild-id liefert 404. Liste zeigt nur lebende Dateien (expires_at IS NULL OR >= now). Items: {id, original_name, type_label, size_bytes, mime_type, session{id,title}, expires_at, expires_in_days, pinned, is_favorite, tags, download_url}.
| Method | URL | Credits | Beschreibung |
|---|---|---|---|
| GET | /files |
0 | Liste mit Filter: ?q (Name), ?type (mime-Teilstring), ?session_id, ?expiring=1, ?sort=newest|oldest|name|size, ?page, ?per_page (≤100) |
| GET | /files/trash |
0 | Soft-deleted Dateien des Teams |
| GET | /files/{id} |
0 | Detail inkl. session + download_url |
| POST | /files/{id}/keep |
0 | Macht die Datei dauerhaft (pinned_at, expires_at=NULL) — gegen 30-Tage-Auto-Ablauf |
| POST | /files/{id}/favorite |
0 | Toggle is_favorite |
| PATCH | /files/{id} |
0 | Update Metadaten: {title?, tags?[]} |
| DELETE | /files/{id} |
0 | Soft-delete (Papierkorb) |
| POST | /files/{id}/restore |
0 | Wiederherstellen aus Papierkorb |
| DELETE | /files/{id}/forever |
0 | Endgültig löschen (NUR aus Papierkorb, sonst 409) |
| POST | /files/bulk/delete |
0 | Body {ids:[]} max 200 |
| POST | /files/bulk/restore |
0 | Body {ids:[]} max 200 |
| POST | /files/bulk/export |
0 | Body {ids:[]} max 500. Async 202 → {job_id} für ZIP |
| GET | /files/exports/{job_id} |
0 | Export-Status: {status, download_url?} |
| POST | /files/{id}/share |
0 | Idempotenter Public-Share-Token. Returns {token, public_url, expires_at, view_count, is_active}. public_url = /share/file/{token} |
| PATCH | /files/{id}/share |
0 | Expiry ändern (404 wenn kein aktiver Share) |
| DELETE | /files/{id}/share |
0 | Public-Share widerrufen |
| Method | URL | Credits |
|---|---|---|
| POST | /agentic/chat |
varies (depends on tools used by the agent) |
Body:
message (string, required, max 10k): User-Anfrage in natürlicher Sprachesession_id (int, optional): bestehende Session fortführenQuery-Parameter:
wait_for_final (bool, default true): bei wait_and_recheck-Handoff (z.B. asynchroner DataForSEO-Pull) pollt der Endpoint serverseitig bis zur finalen Resume-Antwort (max ~480 s). Auf false setzen um sofort die Placeholder-Nachricht zu erhalten.Response 200: {session_id, message_id, text, tool_calls, iterations, credits_used, duration_ms, resumed}
resumed: true heißt: der Master-Agent hat mid-turn wait_and_recheck getriggert und der Endpoint hat erfolgreich auf den Resume-Turn gewartet. Der text ist dann die finale Antwort, nicht der Wartehinweis.
Hinweise:
rankion-agent| Method | URL | Credits | Notes |
|---|---|---|---|
| POST | /ai-scanner/detect |
2 | scan_type (alias mode): quick = sync 200 (heuristic) · deep = async 202 {scan_id,status} → poll status. language (de/en/es/fr) persisted. Every scan stored in /ai-scanner history. Failed deep scan auto-refunds. |
| GET | /ai-scanner/scans/{id} |
— | Poll an AI-detection scan: {scan_id,scan_type,processing_status,score,details,word_count,language,credits_charged}. Team-scoped. |
| POST | /ai-scanner/humanize |
dyn | Humanize an existing article_id (async 202). Cost ceil(words/1000) × {light:2,standard:4,deep:8} — job-deducted, controller pre-flight gates. |
| Method | URL | Credits |
|---|---|---|
| POST | /humanize |
8 |
| GET | /humanize/{batch} · .../documents/{document} |
— |
| Method | URL | Credits |
|---|---|---|
| GET | /content-optimizer · /content-optimizer/{id} · /content-optimizer/optimizations/{id} (alias) |
— |
| POST | /content-optimizer/analyze |
5 |
| POST | /content-optimizer/{id}/apply · /content-optimizer/optimizations/{id}/apply (alias) |
5 |
| DELETE | /content-optimizer/{id} (Soft-Delete, 204) |
— |
Async-Vertrag (Subsystem B P0.3 — fixed 2026-05-09):
POST /v1/content-optimizer/analyze returns 202 mit:
{
"message": "Content optimization started",
"id": 6,
"optimization_id": 6,
"poll_url": "/api/v1/content-optimizer/6"
}
Plus Header Location: /api/v1/content-optimizer/6.
Polling-Pfad: GET /api/v1/content-optimizer/6 (canonical) — der Pfad /optimizations/6 funktioniert auch (Alias-Route, weil das Field heisst optimization_id). Caller können beides.
optimization_id bleibt für Kompatibilität, aber neue Caller sollten id lesen.
| Method | URL | Credits |
|---|---|---|
| GET/POST | /competitor-analyses |
20 (POST) |
| GET | /competitor-analyses/{id} · .../gaps (?is_favorite=&is_dismissed=) |
— |
| PATCH | /competitor-analyses/{id} Body {name} (umbenennen) |
— |
| DELETE | /competitor-analyses/{id} (inkl. Gaps, 204) |
— |
| PATCH | /competitor-analyses/gaps/{id} Body {is_favorite?, is_dismissed?} |
— |
| POST | /competitor-analyses/{id}/rerank-competitors |
— |
Site-wide content-quality crawler. Walks a domain (sitemap-discovered), scores each page on SEO (0–100), content quality (0–100), word count, and emits issues[] = {type:error|warning|info, category, message} plus recommendations[]. Distinct from /site-audit — this module is the content-quality angle, /site-audit is the crawl/issue/grounding angle.
| Method | URL | Credits |
|---|---|---|
| GET/POST | /content-audits |
10 (POST) |
| GET | /content-audits/{id} · .../export · .../pages |
— |
| GET | /content-audits/{id}/compare |
— |
| PATCH | /content-audits/{id}/pages/bulk |
— |
| GET | /content-audit-pages/{id} |
— |
| PATCH | /content-audit-pages/{id}/solved |
— |
| POST | /content-audit-pages/{id}/refresh |
1 |
Audits-listing — filters on GET /content-audits. Query params:
tracking_project_id=<int> — filtert auf Audits eines Tracking-Projects (FK→tracking_projects.id).source_url=<string> — exact-match auf die Audit-URL (Audit ist 1:1 mit URL gescoped).status=running|completed|failedtype=full|quick (UI-Pfade sitemap/single werden nicht über die API gestartet).project_id → 422 (Variante 1 Rename 2026-05-10) — Migration-Hint im Response. tracking_project_id ist die kanonische FK, parallel zum /grounding-Pattern.Listing-Schema (data[].keys): id, tracking_project_id, source_url, type, status, total_pages, avg_seo_score, total_issues, created_at. Detail-Schema (/content-audits/{id}) enthält zusätzlich team_id, user_id, project_id (legacy), tracking_project_id, summary, ….
POST-Body akzeptiert: {source_url, type?:full|quick, tracking_project_id?}. project_id → 422 mit Migration-Hint. tracking_project_id wird via exists:tracking_projects,id validiert und persistiert.
Pages-listing — filters on GET /content-audits/{id}/pages. Query params:
status=open|solved — open is the canonical alias for is_solved=false. Anything other than solved resolves to open.priority=critical|high|medium|lowmin_seo_score=0..100, max_seo_score=0..100issue_type=<string> — matches any issues[].type value (canonical severity buckets are error, warning, info). Implemented as JSON-LIKE on the serialized issues column.sort= one of url|title|seo_score|content_quality_score|word_count|priority|is_solved, dir=asc|desc, per_page= (default 50).Response shape: {progress:{total, solved, open, percent}, pages:{data:[…], current_page, last_page, …}}.
Audit-over-time compare — GET /content-audits/{id}/compare. Read-only, 0 credits, runs no new crawl (pure read of the persisted summary + page issues). Compares {id} (current) against a baseline:
against=<int> — explicit baseline audit id (team-scoped; 422 if not found / foreign team).source_url.baseline: null and null trend deltas (never 404/500).Response shape: {current, baseline, trend:{avg_seo_score_delta, avg_quality_score_delta, total_issues_delta, by_type_delta:{error,warning,info}}, issues:{new:[…], fixed:[…], persistent:[…], by_type_delta}}. Deltas = current − baseline. Issue identity is normalized (url + type + message-core, numbers/brackets stripped) so a re-measured count is not flagged as "new".
Single-page refresh — POST /content-audit-pages/{id}/refresh. 1 credit (credits-auto:1, SSOT config/credits.php flat content_audit_refresh_page). Async re-crawl of one page: sets row status=pending, dispatches RefreshAuditPageJob, returns 202 {page_id, status:queued}. Guarded on the parent audit being completed (else 422) so a mid-flight batch crawl can't re-grab the page. Throttle: 30/min. Poll GET /content-audit-pages/{id} for the pending→processed transition.
Bulk-mark — PATCH /content-audits/{id}/pages/bulk. Throttle: 30/min. Body:
{
"filter": {
"priority": "low",
"status": "open",
"min_seo_score": 0,
"max_seo_score": 50,
"issue_type": "error"
},
"is_solved": true
}
All filter.* fields are optional. filter.status is the FROM-state to match (only flips rows currently in this state). is_solved is required and is the new state to write. Response: {data:{affected:int, is_solved:bool}, meta:{applied_filter:{…}}}. Team-scoped via audit.team_id — cross-team access returns 403.
filter.project_id → 422 (Variante 1 Rename 2026-05-10): Pages haben keinen Project-FK; das Projekt ergibt sich aus dem Parent-Audit. Vorher wurde filter.project_id stumm gedroppt — was Mass-Updates über alle Pages des Audits zog statt nur über die eines Projekts. Jetzt 422 mit klarem Hint.
| Method | URL | Credits |
|---|---|---|
| GET | /community/mentions · /community/mentions/{id} |
— |
| POST | /community/scan Body: {keyword, platforms[]} |
5 |
| GET | /community/alerts |
— |
| Method | URL |
|---|---|
| GET/POST | /site-monitor |
| GET/PUT/DELETE | /site-monitor/{id} |
| POST | /site-monitor/{id}/pause · .../resume · .../check-now |
| GET | /site-monitor/{id}/checks · .../incidents · .../lighthouse |
| Method | URL | Credits |
|---|---|---|
| GET/POST | /bulk-generations Body: {project_id, keywords[], type?, length?} |
15 (POST) |
| GET | /bulk-generations/{id} |
— |
| Method | URL |
|---|---|
| GET/POST | /autopilot Body: {module, frequency, config} |
| PUT/DELETE | /autopilot/{id} |
| GET | /autopilot/{id}/runs real per-run history (keyword, status, geo_score, skip_reason, article link). ?status= filter. Read-only, 0 credits |
Auto-publish quality gate (TODO-059): when settings.auto_publish=true, each run scores the generated content with the deterministic GEOScorerService and only publishes (and pushes to a connected CMS) if total_score >= settings.min_geo_score (default config('autopilot.min_geo_score')=70). Below threshold → kept as a draft + a skipped_low_score run row; never published. Each run records exactly one row (success/skipped_no_credits/skipped_no_keyword/skipped_low_score/failed). Credit per article = FeatureCost::article(word_count); the gate scoring itself is free.
| Method | URL | Credits |
|---|---|---|
| GET/POST | /pipelines Body: {name, stages[]} |
20 (POST) |
| GET | /pipelines/{id} |
— |
| POST | /pipelines/{id}/retry |
— |
| Method | URL |
|---|---|
| GET/POST | /projects |
| GET/PUT/DELETE | /projects/{id} |
| GET/POST | /goals · DELETE /goals/{id} |
| GET | /modules |
| POST | /modules/{module}/activate · .../deactivate |
| 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 |
| Method | URL | Credits |
|---|---|---|
| GET | /google/connections |
— |
| DELETE | /google/connections/{id} |
— |
| GET | /google/gsc/properties |
— |
| POST/DELETE | /google/gsc/properties/{p}/link |
— |
| POST | /google/gsc/properties/{p}/sync |
1 |
| GET | /google/gsc/properties/{p}/metrics |
— |
| GET | /google/ga4/properties |
— |
| POST/DELETE | /google/ga4/properties/{p}/link |
— |
| GET | /google/ga4/properties/{p}/metrics |
— |
| GET | /integrations/google/reviews/status |
— |
| Method | URL |
|---|---|
| GET | /review-sources · /review-sources/{id} |
| DELETE | /review-sources/{id} |
| POST | /review-sources/{id}/rotate-key · .../disconnect · .../reconnect |
| POST | /review-sources/webhook · /review-sources/waitlist |
| POST | /webhooks/reviews/{source} (public, key-auth) |
| Method | URL |
|---|---|
| GET/POST | /blog/categories |
| PUT/DELETE | /blog/categories/{id} |
| GET/POST | /blog/posts |
| GET/PUT/DELETE | /blog/posts/{id} |
| Method | URL |
|---|---|
| PUT | /os/mode |
| GET/POST | /os/preferences |
| GET/POST | /os/files |
| GET/PUT/DELETE | /os/files/{id} |
| POST | /os/files/{id}/move · .../share |
| DELETE | /os/files/{id}/share |
| GET/POST | /os/folders |
| GET/PUT/DELETE | /os/folders/{id} |
| POST | /os/folders/{id}/move |
| GET | /os/desktop · /os/spotlight |
| Method | URL |
|---|---|
| GET | /team |
| GET | /credits · /credits/history |
| GET | /dashboard |
| GET | /admin/api-costs (Admin only) |
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?} |
— |
Use-Case: Vor dem ersten Checkout das Rechnungsprofil setzen, damit Stripe-Rechnungen korrekte Firmenanschrift + USt-IdNr. (Reverse-Charge) tragen.
Idiom: GET /billing/profile (Ist-Stand lesen) → PUT /billing/profile (vollständiges Profil schreiben, 422 bei ungültigem USt-ID-Format/fehlenden Pflichtfeldern) → der PUT synct synchron nach Stripe; vat_id_status (Stripe-Echo) erscheint beim nächsten GET. Single-Resource (kein Bulk, kein Polling).
Anti-Patterns: Kein wiederholtes PUT in Schleife (jeder PUT triggert einen Stripe-Sync). USt-ID nicht selbst „verifizieren" — das macht Stripe; nur den vat_id_status lesen.
Cross-Module: /billing (Checkout/Invoices) liest dasselbe Profil; InvoiceMail geht an billing_email ?? owner. Modul-Doc: docs/modules/billing.md.
Mitglieder-Verwaltung + Einladungen eines Teams. Alle Endpoints team-scoped auf currentTeam() des Token-Users; Gate manage members (Owner/Admin). Delegiert an TeamMembershipService (Single-Source der Mutationen). Fremdes-Team-Member/-Einladung → 404; Service-Regelverletzung (z. B. Owner ändern, sich selbst entfernen, Plan-Rolle, Sitzplatz-Limit) → 422 mit message.
| Method | URL | Beschreibung | Gate | Credits |
|---|---|---|---|---|
| GET | /team/members |
Mitglieder-Liste {id, name, email, role, is_owner, last_active_at}. Owner wird immer als owner ausgewiesen. |
manage members | — |
| PATCH | /team/members/{user} |
Rolle ändern. Body {role}. admin darf nur der Owner vergeben/entziehen; Owner-Rolle nicht änderbar; eigene Rolle nicht änderbar. 404 fremdes Team, 422 Plan-Rolle/Regelverstoß. |
manage members | — |
| DELETE | /team/members/{user} |
Mitglied entfernen (+ zugehörige project_user-Einträge; current_team_id des Entfernten wird genullt, wenn es dieses Team war). Owner/Selbst nicht entfernbar (422). |
manage members | — |
| GET | /team/invitations |
Offene (nicht angenommene, nicht abgelaufene) Einladungen {id, email, role, expires_at}. |
manage members | — |
| POST | /team/invitations |
Einladen. Body {email, role}. 422 bei Sitzplatz-Limit oder im Plan nicht verfügbarer Rolle. Versendet Einladungs-Mail (7 Tage gültig). 201. |
manage members | — |
| DELETE | /team/invitations/{id} |
Einladung widerrufen. Bereits angenommene → 422. 404 fremdes Team. |
manage members | — |
Use-Case: Team-Sitzplätze verwalten — Kollegen mit passender Rolle einladen, Rollen anpassen, ausgeschiedene Mitglieder entfernen.
Idiom: GET /team/members + GET /team/invitations (Ist-Stand) → POST /team/invitations {email, role} (einladen; 422 bei Limit/Plan-Rolle) → nach Annahme erscheint die Person in /team/members, die Einladung verschwindet aus /team/invitations → PATCH /team/members/{user} für spätere Rollen-Änderungen, DELETE zum Entfernen. Single-Resource, kein Bulk, kein Polling (Annahme passiert async durch den Eingeladenen via Accept-Link).
Anti-Patterns: Keine Owner-Rolle per PATCH/POST vergeben (nur via Ownership-Transfer). Nicht wiederholt dieselbe E-Mail einladen — offene Einladungen sind idempotent (dieselbe Einladung wird wiederverwendet, kein Duplikat). Rolle nicht aus einem hardcoded Set wählen — nur Rollen aus RoleOptions::forTeam() (plan-abhängig) sind gültig, sonst 422.
Cross-Module: Rollen/Permissions stammen aus TeamRole + HasTeamPermissions::canDo(); UI-Pendant ist die Team-Settings-Surface. Modul-Doc: docs/modules/team-members.md.
Auditiert eine einzelne Landingpage: 3 Source-Screenshots (Desktop/Tablet/Mobile mit echter UA-Emulation), Lighthouse-Metriken, Opus 4.7 Multimodal-Vision-Analyse plus bis zu 3 gpt-image-2 KI-Renders der optimierten Variante (Desktop ist die Design-Basis, Tablet+Mobile sind responsive Adaptionen). Authentifizierung: Authorization: Bearer <token>, alle Endpoints team-scoped.
| Method | URL | Credits | Beschreibung |
|---|---|---|---|
| POST | /page-audit |
30 + 15 | Audit starten. Body: {url, tracking_project_id?, persona?} → 202 + {id, status:"pending", url} |
| GET | /page-audit/{id} |
— | Detail mit 2 Bild-URLs (Desktop-Source + Desktop-Ideal) + opus_analysis + Lighthouse |
| GET | /page-audits |
— | Paginierte Liste. ?per_page=25&tracking_project_id= |
Pipeline-Status: pending → scraping → screenshotting → analyzing → completed. Der Desktop-KI-Render läuft in einem separaten Background-Job nach completed; ideal_screenshot_url füllt sich anschließend.
Polling-Pattern:
# 1) Audit starten
ID=$(curl -s -X POST $BASE/page-audit \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"url":"https://example.com/landing"}' | jq -r '.id')
# 2) Pollen bis completed + Desktop-Ideal-Render fertig (Hauptflow ~1-2 min, Desktop-Render +2-3 min)
while true; do
R=$(curl -s -H "Authorization: Bearer $TOKEN" $BASE/page-audit/$ID)
STATUS=$(echo "$R" | jq -r '.status')
IDEAL=$(echo "$R" | jq -r '.ideal_screenshot_url // "null"')
echo "$STATUS desktop_ideal=$([ "$IDEAL" = null ] && echo no || echo yes)"
[ "$STATUS" = completed ] && [ "$IDEAL" != null ] && break
sleep 30
done
Response-Shape (GET /page-audit/{id} nach completed):
{
"id": 10,
"tracking_project_id": null,
"url": "https://www.example.com/landing",
"status": "completed",
"error_message": null,
"screenshot_url": "https://rankion.ai/storage/page-audits/10/screenshot.png",
"ideal_screenshot_url": "https://rankion.ai/storage/page-audits/10/ideal.png",
"lighthouse": { "performance": 78, "seo": 92, "accessibility": 88, "best_practices": 83 },
"analysis": {
"user_intent": "1-Satz Search-Intent",
"persona_fit": 75,
"trust_score": 72,
"layout_score": 68,
"cta_score": 65,
"problem_solution_clarity": 70,
"above_the_fold_quality": 60,
"mobile_friendliness_visual": 80,
"personas": [
{ "name": "...", "who": "...", "primary_motivation": "...",
"pain_points": ["..."], "goals": ["..."],
"fit_score": 75, "fit_reason": "..." }
],
"critical_issues": [
{ "severity": "high|medium|low", "title": "...", "explanation": "...", "evidence": "..." }
],
"improvement_suggestions": [
{ "area": "headline|cta|trust|layout|copy|visuals|forms|navigation|seo|accessibility",
"priority": "high|medium|low",
"title": "...", "description": "...", "example": "before/after micro-copy" }
],
"summary": "3-5 Satz Executive-Summary",
"headline_rewrite": "stärkerer H1-Vorschlag oder null"
},
"gpt_image_used": true,
"credits_used": 75,
"started_at": "2026-04-27T19:28:58Z",
"completed_at": "2026-04-27T19:30:22Z"
}
Status-Codes: 202 (gestartet), 200 (Detail/List), 403 (Cross-Team / Cross-Project), 404 (Audit nicht gefunden), 422 (Validation: url Pflicht, max 500 Zeichen).
Use-Cases:
analysis.improvement_suggestions (sortiert nach priority) lesen → Änderungen am Source-Page anwenden → Re-Audit → Score-Diff vergleichen.ideal_screenshot_url (Desktop+Mobile) als Visual-Reference in den Design-Pipeline-Iteration füttern.analysis.personas[].pain_points + goals → gezielte Headline-/CTA-Varianten schreiben → re-audit.analysis.headline_rewrite ist ein validierter Headline-Replacement — kann direkt in den Live-Page übernommen werden.Auto-Generator für Tracking-Project-Reports (Markdown-Output via GenerateTrackingProjectReportJob) und ein Cross-Modul-Korrelations-Endpoint, der Site-Audit, Rank-Tracker, Backlinks und AVI verknüpft. Authentifizierung: Authorization: Bearer <token>, alle Endpoints team-scoped.
| Method | URL | Credits | Beschreibung |
|---|---|---|---|
| POST | /tracking-projects/{id}/generate-report |
10 | Report generieren (async). 202 + {report_id, tracking_project_id, status, status_endpoint}. Rate-Limit: 1 Request / 30 Min / Project — sonst 429 {error:"rate_limited"}. |
| GET | /tracking-projects/{id}/reports |
— | Letzte 50 Reports des Projects (sortiert nach created_at desc). |
| GET | /reports/{id} |
— | Report-Detail inkl. markdown_content und summary_json. |
Status-Werte: pending → generating → completed (oder failed).
Polling-Pattern:
# 1) Report dispatchen
RID=$(curl -s -X POST $BASE/tracking-projects/$PID/generate-report \
-H "Authorization: Bearer $TOKEN" | jq -r '.report_id')
# 2) Pollen bis completed
while true; do
S=$(curl -s -H "Authorization: Bearer $TOKEN" $BASE/reports/$RID | jq -r '.status')
echo "Status: $S"
[ "$S" = completed ] && break
[ "$S" = failed ] && exit 1
sleep 5
done
# 3) Markdown extrahieren
curl -s -H "Authorization: Bearer $TOKEN" $BASE/reports/$RID | jq -r '.markdown_content' > report.md
Response — POST /tracking-projects/{id}/generate-report (202):
{
"report_id": 42,
"tracking_project_id": 7,
"status": "pending",
"status_endpoint": "/v1/reports/42",
"message": "Report dispatched"
}
Response — GET /tracking-projects/{id}/reports (200):
{
"data": [
{
"id": 42,
"tracking_project_id": 7,
"team_id": 3,
"user_id": 12,
"status": "completed",
"credits_used": 10,
"generated_at": "2026-04-27T18:14:22+00:00",
"created_at": "2026-04-27T18:13:40+00:00",
"updated_at": "2026-04-27T18:14:22+00:00"
}
]
}
Response — GET /reports/{id} (200):
{
"id": 42,
"tracking_project_id": 7,
"team_id": 3,
"user_id": 12,
"status": "completed",
"markdown_content": "# Tracking-Report Project XY\n\n## Executive Summary\n…",
"summary_json": {
"headline_kpis": { "avi_score": 64, "delta_30d": "+8", "top10_keywords": 14 },
"wins": [ "..." ],
"risks": [ "..." ]
},
"credits_used": 10,
"error_message": null,
"generated_at": "2026-04-27T18:14:22+00:00",
"created_at": "2026-04-27T18:13:40+00:00",
"updated_at": "2026-04-27T18:14:22+00:00"
}
Status-Codes: 202 (Report dispatched), 200 (List/Detail), 403 (Cross-Team / Cross-Project), 404 (Report/Project nicht gefunden), 429 (Rate-Limit: max 1 / 30 Min).
Aggregiert drei Korrelations-Datenpunkte für einen TrackingProject in einem Roundtrip — Risk-Map (Site-Audit × Rank-Tracker), Backlinks-Velocity × AVI-Trend (Pearson, 30d), Smart-Todos.
| Method | URL | Credits | Beschreibung |
|---|---|---|---|
| GET | /tracking-projects/{id}/correlation |
— | Cross-Module-Korrelation Snapshot |
Response — GET /tracking-projects/{id}/correlation (200):
{
"data": {
"site_audit_ranking_risks": [
{
"keyword_id": 91,
"keyword": "best crm",
"search_volume": 4400,
"position": 5,
"url": "https://example.com/best-crm",
"issues_count": 7,
"critical_count": 2,
"medium_count": 3,
"risk_score": 8.4,
"issues": [
{ "type": "missing_h1", "severity": "critical", "message": "..." }
]
}
],
"backlinks_av_correlation": {
"correlation_coefficient": 0.612,
"days_with_data": 14,
"velocity_total_30d": 23,
"avi_data_points_30d": 18,
"backlink_events_30d": 41,
"observation": "Moderat positive Korrelation: Backlink-Velocity beeinflusst AVI."
},
"smart_todos": [
{
"priority_score": 84,
"estimated_impact": "medium",
"title": "Page-Issues fixen die Top-Ranking gefährden",
"why": "URL …/best-crm rankt für \"best crm\" auf Position 5 — hat aber 7 Site-Audit-Issues (missing_h1, ...).",
"reference_type": "tracking_keyword",
"reference_id": 91,
"url": "https://example.com/best-crm",
"keyword": "best crm",
"position": 5,
"risk_score": 8.4
}
]
}
}
Status-Codes: 200 (Erfolg), 403 (Cross-Team / Cross-Project), 404 (Project nicht gefunden).
# 1) Analyse starten
RESP=$(curl -s -X POST $BASE/tracking-projects/analyze \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"mode":"domain","domain":"example.com","language":"de","country":"DE"}')
PID=$(echo $RESP | jq -r .project_id)
# 2) Polling bis completed
while [ "$(curl -s $BASE/tracking-projects/$PID/analysis-status \
-H "Authorization: Bearer $TOKEN" | jq -r .analysis_status)" != "completed" ]; do
sleep 3
done
# 3) Atomic Aktivierung
curl -X POST $BASE/tracking-projects/$PID/activate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"keywords":["best crm","crm software"],
"competitors":[{"domain":"hubspot.com"}],
"prompts":[{"prompt":"What is the best CRM?","category":"recommendation"}],
"platforms":["chatgpt","perplexity"],
"frequency":"weekly",
"with_search":true,
"reality_check_enabled":true
}'
curl "$BASE/explorer/176007?include=related,questions" \
-H "Authorization: Bearer $TOKEN"
# returns: keyword, search_volume, kd_score, parent_topic, traffic_potential,
# serp_urls, monthly_searches[12], related[90], questions[11], cache_hit
# Generate
curl -X POST $BASE/articles/$ART/generate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"keyword":"AI coding tools","article_type":"blog","target_length":1500}'
# Publish to WordPress (CMS-ID 1 vorausgesetzt)
curl -X POST $BASE/articles/$ART/publish \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"cms_integration_id":1}'
# Liste mit Filter
curl "$BASE/tracking-projects/$PID/cited-sources?outreach_status=neu" \
-H "Authorization: Bearer $TOKEN"
# Status updaten
curl -X PATCH $BASE/tracking-projects/$PID/cited-sources/$SID \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"outreach_status":"anfrage_gestellt","notes":"E-Mail an redaktion@..."}'
# CSV-Export
curl "$BASE/tracking-projects/$PID/cited-sources/export?from=2026-01-01" \
-H "Authorization: Bearer $TOKEN" -o sources.csv
credits:N und/oder Job-Dispatch geben 202 Accepted mit Job-ID. Polling via Detail-Endpoint bis processing_status == 'ready'.?cache=false (z.B. Explorer). Inline 50 Credits Charge.?from=&to=&status=&platform=&search=&sort=&limit=&page=.?per_page=N&page=N. Response: {data:[], meta:{total, per_page, current_page, last_page}}.?include=related,questions (kommagetrennt) inlined Sub-Resources statt separater Roundtrips.Diese Datei spiegelt routes/api.php. Die maschinenlesbare SSOT der /v1-Route-Liste
ist docs/API_ROUTES.generated.json — pro Endpoint method, uri (doc-Stil, ohne
api-Prefix), name, action (Controller@method | Closure) und der aus der
Middleware geparste credits + credits_mode (gate = credits:N, auto =
credits-auto:N). Sortiert nach (uri, method), diff-stabil.
# Manifest (neu) generieren — nach jeder Route-Änderung ausführen + committen:
php artisan api:generate
# CI-Gate: prüft (ohne zu schreiben), ob das committete Manifest aktuell ist.
# Exit 1 + Diff-Summary bei Drift:
php artisan api:generate --check
# Schnellzählung der Live-/v1-Endpoints (Fallback):
php artisan route:list | grep "api/v1" | wc -l
Quelle: App\Services\Api\RouteIntrospector (spiegelt exakt
ApiDocsController::countV1Endpoints()). Der Parity-Test
tests/Feature/Docs/ApiDocParityTest.php erzwingt: (1) Introspector-Count ==
Controller-Count, (2) committetes Manifest aktuell (sonst „Run php artisan api:generate"), (3) jede /v1-Route ist in dieser Datei dokumentiert ODER auf der
expliziten KNOWN_UNDOCUMENTED-Baseline (fängt neue undokumentierte Routen), (4)
jede in dieser Datei genannte /v1-Pfad-Referenz löst auf eine echte Route auf, (5)
Modul-Doc-Freshness (README-Link↔Datei-Parität + Soft-Check der /api/v1-Referenzen
in docs/modules/*.md).
Live-Tabellen mit Beispielen: https://rankion.ai/settings/api-documentation
Public-share API for AI chat sessions. Owner creates a share link from /api/v1/chat-shares, recipient views it via /share/{token} (public, noindex). All endpoints require auth:sanctum and team-scoping.
| Method | URL | Description | Credits |
|---|---|---|---|
| GET | /v1/chat-shares |
List own share links (paginated, 25/page) | 0 |
| POST | /v1/chat-shares |
Create new share for owned session (default 7-day expiry) | 0 |
| GET | /v1/chat-shares/{id} |
Detail + stats | 0 |
| PATCH | /v1/chat-shares/{id} |
Update settings | 0 |
| DELETE | /v1/chat-shares/{id} |
Revoke (soft delete; row preserved) | 0 |
| GET | /v1/chat-shares/{id}/views |
Paginated view log (DSGVO-anonymized) | 0 |
| GET | /v1/agentic/sessions/{session}/shares |
Shares for a specific session | 0 |
Body:
session_id (required, integer): id of an AiChatSession owned by the authenticated usertitle (optional, string)expires_at (optional, ISO datetime, must be future)password (optional, string min 4)max_views (optional, integer min 1)show_user_messages (default true)show_assistant_messages (default true)show_tool_results (default false — recipients see only inputs+outputs, not the tool-call cards)show_reasoning_steps (default false)show_uploaded_files (default true)true, else 422.Response 201:
{
"data": {
"id": 1, "token": "<40 chars>",
"url": "https://rankion.ai/share/<token>",
"title": "...", "session_id": 42,
"expires_at": "...", "frozen_at": null,
"has_password": false, "max_views": null,
"view_count": 0, "unique_viewer_count": 0,
"last_viewed_at": null, "revoked_at": null,
"status": "active",
"show_user_messages": true,
"created_at": "..."
}
}
Same fields as POST plus freeze: bool (true = freeze content snapshot, false = back to live).
Soft revoke. Sets revoked_at. Public access returns 410. Row stays in DB until daily prune job (90 days post-revoke by default, configurable via CHAT_SHARES_RETENTION_DAYS).
GET /share/{token} — public, no auth, returns HTML. noindex enforced via meta + X-Robots-Tag header + robots.txt disallow.
POST /share/{token}/unlock — password-form submission; throttle 5/min. Sets path-scoped httpOnly cookie on success.
GET /share/{token}/print — clean print-CSS variant of the share view.
Read-only API for the agentic-superpowers Brainstorm → Plan → Execute pipeline. Plans are created/mutated EXCLUSIVELY by the master agent's pipeline tools (build_plan, complete_step, fail_step, skip_step, complete_plan, clarify_intent); the HTTP API exposes only inspection. All endpoints require auth:sanctum and team-scoping (Policy: view only — no update/delete exposed).
| Method | URL | Description | Credits |
|---|---|---|---|
| GET | /v1/agentic/sessions/{session}/plans |
Plan history for a session (paginated 50/page, ordered desc by created_at) | 0 |
| GET | /v1/agentic/plans/{plan} |
Plan detail (status, denormalized counters, brainstorm_summary) | 0 |
| GET | /v1/agentic/plans/{plan}/steps |
All steps with output_summary, output_data (≤64kB JSON), tool_calls_made, credits_used, latency_ms, retry_count, last_error, subagent_session_id | 0 |
| POST | /v1/agentic/feedback |
Persists 👍/👎 feedback on an assistant message. Body: {message_id: int, direction: 'up'|'down'}. Idempotent upsert keyed by (user_id, message_id) — second click overwrites direction. Returns 404 unknown_msg, 403 not_yours, 422 invalid direction. Response: {ok: true, direction} |
0 |
{
"data": {
"id": 12, "session_id": 42, "goal": "Konkurrenz-Analyse example.com",
"brainstorm_summary": null, "strategy": "linear",
"status": "executing",
"current_step_id": 35,
"total_steps": 4, "completed_steps": 1, "failed_steps": 0,
"started_at": "2026-05-01T08:30:00Z", "completed_at": null,
"total_tool_calls": 9, "total_credits_used": 5,
"created_at": "2026-05-01T08:29:55Z"
}
}
{
"data": [
{
"id": 35, "step_index": 0,
"label": "Top-10 SERP-Konkurrenten ermitteln",
"description": "...",
"complexity": "trivial", "assigned_agent": null,
"status": "completed", "retry_count": 0, "last_error": null,
"output_summary": "10 Konkurrenten: ahrefs.com, semrush.com, ...",
"output_data": { "competitors": ["ahrefs.com", "..."] },
"tool_calls_made": 1, "credits_used": 1, "latency_ms": 3120,
"subagent_session_id": null,
"started_at": "...", "completed_at": "..."
}
]
}
status values: pending | in_progress | completed | failed | skipped (steps); planning | executing | completed | failed | aborted (plans).
assigned_agent values: null (master itself) | master | analyze | explore | research | action (Subagent-Persona).
Maschinenlesbarer Vertrag für Subsystem A + C — fixed 2026-05-09.
GET /api/v1/openapi.yamlOpenAPI 3.1.0 Spec (public, kein Auth nötig). 5-min CDN-Cache. Generiert TypeScript/Python/PHP-SDKs aus z.B. openapi-generator-cli:
npx @openapitools/openapi-generator-cli generate \
-i https://rankion.ai/api/v1/openapi.yaml \
-g typescript-fetch \
-o ./rankion-sdk-ts
Schema-Coverage:
/grounding/analyze, /audits/{id}, /audits, /batch, /batches/{id}, /batches/{id}/results.ndjson, /sitemap-audit/content-optimizer/{id} (canonical + alias)/agentic/chat, /agentic/sessions/{session}GroundingAudit, GroundingBatch, Finding, Tier, StructuredError, AgenticChatRequest/Response, AgenticSessionStatusAndere Endpoints sind weiterhin nur in dieser Markdown-Reference dokumentiert; eine vollständige OpenAPI-Export-Phase ist auf der Roadmap.
Alle code Werte aus dem error: {code, ...} Envelope (Subsystem A + C):
| HTTP | code | Endpoint | Beschreibung |
|---|---|---|---|
| 422 | context_overflow |
/agentic/chat | 1M-Token-Limit überschritten. Body: limit_tokens, used_tokens, suggested_action, max_urls_per_call |
| 502 | anthropic_api_error |
/agentic/chat | Anthropic-Upstream 5xx. Body: http_status, message |
| 404 | audit_not_found |
/grounding/audits/{id} | ID unbekannt ODER fremdes Team (404 statt 403 gegen Enumeration) |
| 410 | audit_expired |
/grounding/audits/{id} | TTL gepruned (30d soft + 60d hard) |
| 404 | batch_not_found |
/grounding/batches/{id} | ID unbekannt ODER fremdes Team |
| 422 | invalid_url |
/grounding/* | Kein gültiger http(s)-URL |
| 422 | invalid_framework |
/grounding/* | Framework außerhalb [v1.5, eeat, people-first] |
| 422 | too_many_urls |
/grounding/batch | Batch > 500 URLs |
| 402 | insufficient_credits |
/grounding/, /content-optimizer/ | Team-Balance < required |
| 503 | sitemap_unreachable |
/grounding/sitemap-audit | Sitemap-Parse fehlgeschlagen |
| 404 | session_not_found |
/agentic/sessions/{id} | Triple-scoped IDOR-Schutz greift |
| 429 | rate_limited |
(alle Endpoints mit throttle:) |
Standard Laravel-Throttle. Header Retry-After setzt Wartezeit |
Jeder Endpoint mit throttle:N,M Middleware liefert auf erfolgreiche Responses:
X-RateLimit-Limit: N (Anfragen pro Window)X-RateLimit-Remaining: <int> (verbleibend im aktuellen Window)X-RateLimit-Reset: <epoch> (Unix-Timestamp wann Counter resettet)Bei 429: zusätzlich Retry-After: <seconds>. Counters per Sanctum-Token, nicht per IP.
Throttle-Werte pro Endpoint stehen in der Endpoint-Tabelle der jeweiligen Sektion (oben). Aktuelle Werte:
/grounding/analyze: 30/min · /grounding/batch, /sitemap-audit: 5/min · /grounding/audits/{id}, /batches/{id}: 120/min · /grounding/audits (Liste): 60/min · /grounding/batches/{id}/results.ndjson: 30/min · /agentic/sessions/{session}: 120/min · /agentic/chat: kein expliziter Throttle (Cap durch interne max_iterations + Anthropic-Token-Limits).Drei Härtungen am /api/v1/agentic/chat Flow für robustere programmatische Caller — fixed 2026-05-09.
context_overflow ErrorWenn der Agent das 1M-Token-Kontext-Limit hittet, returnt /agentic/chat jetzt HTTP 422 mit strukturiertem Error-Envelope statt 200+German-Text:
{
"session_id": 1201,
"error": {
"code": "context_overflow",
"limit_tokens": 1000000,
"used_tokens": 1042000,
"suggested_action": "split_request",
"max_urls_per_call": 1
},
"text": "⚠️ Diese Anfrage ist zu komplex geworden …"
}
Programmatischer Retry ohne String-Matching auf Deutsch. Bei Anthropic-API-Errors (5xx upstream) → HTTP 502 mit error.code = "anthropic_api_error".
tool_policy im RequestRestriktiere welche Tools der Master-Agent in einem Turn nutzen darf — ideal für Audit-Loops, die nur 1-Tool-Calls wollen ohne Token-Blow:
{
"message": "Audit https://example.com",
"tool_policy": {
"allow": ["analyze_grounding_page"],
"deny": ["web_scrape", "extract_from_urls"],
"max_tool_calls": 1
}
}
Server-side Enforcement: gesperrte Tools liefern Tool-Result {success:false, error:"tool X blocked by tool_policy.deny", policy_blocked:true} — der Agent ignoriert sie und liefert ein Teil-Resultat statt das Limit zu sprengen.
/agentic/chat blockiert standardmäßig NICHT mehr bis zum finalen Resume. Wenn der Agent mid-turn wait_and_recheck ausgelöst hat, returnt der Endpoint HTTP 202 sofort:
{
"session_id": 1201,
"agentic_status": "waiting",
"eta_seconds": 120,
"poll_url": "/api/v1/agentic/sessions/1201",
"resumed": false,
"text": "⏳ Ich warte auf Hintergrund-Daten…"
}
Caller polled dann GET /api/v1/agentic/sessions/{id}:
{
"session_id": 1201,
"agentic_status": "completed",
"message_id": 4567,
"text": "Audit fertig: Score 75/100…",
"last_message_at": "2026-05-09T14:32:01+00:00",
"is_terminal": true
}
agentic_status: null (idle) | running | waiting | awaiting_user | failed | completed. is_terminal=true heißt: keine weitere Hintergrund-Arbeit pending (awaiting_user ist NICHT terminal — es wartet auf eine User-Antwort).
Opt-in legacy blocking: POST /agentic/chat?wait_for_final=true → server polled bis 480s und liefert das finale Result inline (alte Verhalten).
ask_user_question, 2026-06-10)Ruft der Agent das ask_user_question-Tool (oder ein Plan-Confirmation-Gate), pausiert die Loop mit agentic_status='awaiting_user'. POST /agentic/chat, GET /agentic/sessions/{id} und POST /agentic/sessions/{id}/answer-question hängen dann zusätzlich an:
{
"agentic_status": "awaiting_user",
"is_terminal": false,
"awaiting_user": true,
"pending_question": "Soll ich den teuren Voll-Pull starten (120 Credits)?",
"pending_question_options": [
{"label": "Ja, starten (Empfohlen)", "description": "Kostet 120 Credits"},
{"label": "Nein, abbrechen", "description": ""}
],
"answer_url": "/api/v1/agentic/sessions/1201/answer-question"
}
Antworten: POST /agentic/sessions/{id}/answer-question mit { "option_label": "Ja, starten (Empfohlen)" } → resumed die Loop synchron (Response-Shape = /agentic/chat; enthält erneut die awaiting_user-Felder, falls direkt eine Folge-Frage kommt). Damit funktionieren Rückfragen + Aktions-Bestätigungen via API genauso wie im Chat-UI. Die Entscheidung WANN der Agent fragt ist advisory (Tool-Beschreibung empfiehlt es bei >50-Credit-Ops) — kein erzwungener Gate.
Async-Pipeline für LLM-Citation-Readiness-Audits über öffentliche URLs. Single + Bulk + Sitemap mit NDJSON-Stream und HMAC-signierten Webhook-Callbacks.
Frameworks:
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| POST | /v1/grounding/analyze |
Single-URL-Audit dispatchen. Body: {url, frameworks?, tracking_project_id?}. Throttle: 30/min. Returns 202 + {audit_id, status, estimated_seconds, poll_url} + Location: Header. |
1 |
| GET | /v1/grounding/audits/{id} |
Polling/Result. Returns {audit_id, status, score, tier:{name,threshold_min,threshold_max}, frameworks{}, findings[], raw_text, duration_ms, credits_used, started_at, completed_at, expires_at}. 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 |
| Score | Tier |
|---|---|
| 0–25 | not-grounded |
| 26–50 | weak |
| 51–75 | partial |
| 76–100 | grounded |
Jedes Audit liefert findings[] mit Objekten: {id, severity (critical|high|medium|low|info), framework (v1.5|eeat|people-first), title, description, fix:{type, action, details}, spec_url}. Zusätzlich raw_text (Markdown) für UI-Render.
Server POSTet zu callback_url mit Header X-Rankion-Signature: sha256=<hex> (HMAC-SHA256 über Body, Key = callback_secret) + X-Rankion-Webhook-Id: <uuid>. 4 Retries (60/300/1800/7200s) bei 5xx oder 429. Verifikation:
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $callbackSecret);
hash_equals($expected, $request->header('X-Rankion-Signature'));
| HTTP | code |
|---|---|
| 422 | invalid_url, invalid_framework, too_many_urls |
| 402 | insufficient_credits |
| 404 | audit_not_found, batch_not_found |
| 410 | audit_expired |
| 503 | sitemap_unreachable |
| 429 | rate_limited |
Beispiel — Single-URL Audit:
ID=$(curl -sX POST -H "Authorization: Bearer $RANKION_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","frameworks":["v1.5"]}' \
https://rankion.ai/api/v1/grounding/analyze | jq -r .audit_id)
# Poll
sleep 30
curl -H "Authorization: Bearer $RANKION_API_TOKEN" \
https://rankion.ai/api/v1/grounding/audits/$ID
Beispiel — Sitemap-Batch:
curl -X POST -H "Authorization: Bearer $RANKION_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"sitemap_url":"https://example.com/sitemap.xml","filter":{"path_contains":"/blog","limit":50},"callback_url":"https://my-app/hook"}' \
https://rankion.ai/api/v1/grounding/sitemap-audit
Knowledge-Base intern: docs/knowledge/grounding-pages-geo.md. Modul-Doku: docs/modules/grounding-audit.md.
Read-only Discovery-Endpoint für die GEO-/LLM-Citation-Kriterien — die Single Source of Truth (App\Support\Geo\GeoCriteriaRegistry), die den Grounding-Audit, den GEO-Content-Score, die Generierungs-Prompts (ContentGeneratorService — live) und die Off-Page-/Demand-Achse (OffPageReadinessScorer) speist. Lässt GEO-Optimizer-Agenten entdecken, welche Signale Rankion prüft, mit welchem Engine-Signal, welcher Evidenz-Stufe und an welchen Oberflächen (audit/score/prompt/offpage). Synchron, 0 Credits, kein Polling.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /v1/geo-criteria |
GEO-Kriterien-Katalog. Optional ?category=<cat> (z.B. entity, extraction, schema, evidence, eeat, indexability, offpage). Returns {data: [{id, label, description, category, engine_signal, dimension, evidence_tier, study_source, score_weight, shapes[]}]}. Read-only. |
0 |
| GET | /v1/tracking-projects/{id}/off-page-readiness |
Off-Page-/Demand-Achsen-Score eines Tracking-Projekts. Team-scoped, read-only. | 0 |
Response-Felder (/v1/geo-criteria):
id — stabile Kriterien-ID, identisch mit der Grounding-Finding-ID bzw. dem GEO-Score-Breakdown-Key (gp.h1.entity_only, geo.evidence.citation_density, geo.offpage.brand_demand, …).engine_signal — referenziertes Engine-Capability-Matrix-Signal (config/engines.php).dimension — technical (Signal indexability/crawlability) oder content. Abgeleitet, nie driftend.evidence_tier — A/B Default-Hint. Der wirksame Tier eines konkreten Findings kommt aus /v1/grounding/audits/{id} (Laufzeit-Auflösung über die Engine Capability Matrix).shapes[] — Oberflächen, an denen das Kriterium teilnimmt: audit (Page- oder Site-Root-Detektor), score, prompt, offpage (Demand-Achse, geo.offpage.*).GET /v1/tracking-projects/{id}/off-page-readiness — aggregierter Off-Page-Score eines Tracking-Projekts (gewichtetes Mittel über die data_available-Signale). Team-scoped: Fremd-Team → 403, unbekannte ID → 404. 0 Credits.
{
"score": 87,
"data_available": true,
"components": {
"geo.offpage.brand_demand": { "score": 100, "data_available": true, "meta": { "brand_keyword": "rankion", "search_volume": 12000 }, "weight": 25 },
"geo.offpage.third_party_mentions": { "score": 70, "data_available": true, "meta": { "third_party_count_90d": 30, "window_days": 90 }, "weight": 20 },
"geo.offpage.source_affinity": { "score": 100, "data_available": true, "meta": { "distinct_third_party_types": 3, "has_wiki": true }, "weight": 15 }
},
"meta": { "tracking_project_id": 17, "credits": 0 }
}
score ist int|null — null ⇒ noch keine Daten, NICHT 0. data_available unterscheidet beide Fälle; jedes components.* trägt sein eigenes data_available + meta.reason.brand_demand braucht brand_name + passendes Tracking-Keyword mit search_volume; third_party_mentions braucht aktivierten Community-Monitor (CommunityProject am selben Project); source_affinity braucht Citation-Historie (cited_sources).community_project_id (Drittseiten-Mentions) vs. tracking_project_id (Brand-Demand + Quellen-Affinität) pro Faktor auf — via CommunityProject.project_id == TrackingProject.project_id.Accept: application/vnd.rankion.v2+json): {data: {score, data_available, components}, meta}.Read-only: Detector-/Factor-Objekte werden nie serialisiert — nur skalare Metadaten. Den Katalog einmal ziehen, dann damit Grounding-Audit (/v1/grounding/analyze), Content-Optimizer bzw. Off-Page-Readiness ansteuern.
Skill-Manifeste: grounding.geo-criteria, tracking.off-page-readiness. Modul-Doku: docs/modules/geo-criteria-catalog.md.
Public + internal wiki content. Markdown-Files in docs/wiki/ werden via php artisan wiki:sync (auto bei jedem Deploy) in die DB gespiegelt. Slug-Hierarchie via /: modules/backlinks. Wiki-interne Links via [[slug]] oder [[slug|Anzeigetext]].
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /v1/wiki/pages |
Liste aller Pages (paginiert 50/Seite). Filter: ?category=modules, ?visibility=public|internal. Sanctum-User sehen auch internal-Pages. |
0 |
| GET | /v1/wiki/pages/{slug} |
Single Page mit body_md, body_html, toc[] (level/text/anchor), related[]. Slug akzeptiert Slashes für Hierarchie. |
0 |
| GET | /v1/wiki/search?q=... |
Top-20 FULLTEXT-Treffer (title+description+body_md). Title-Boost 2×. Snippets mit <mark>-highlighted Tokens. Min. 2 Zeichen. |
0 |
Public-Reader: https://rankion.ai/wiki/{slug} — server-rendered HTML, Sidebar+TOC, /-Shortcut für Search-Focus. visibility: internal → 401 für anonyme User.
Master-Agent-Integration: query_wiki(query) Master-Tool returnt Top-3 mit vollen URLs — Trigger bei "wie geht X", "wie aktiviere ich Y", "was ist Z?".
Tiefenanalyse über alle 6 Mining-Schichten (Atomisierung → Quellenscan → Cross-Pattern → Faktencheck → Forecast → Action). Liefert pro Projekt einen 9-Sektionen-Report (Executive Summary, Faktenfehler, Konsens-Narrative, Persona-Lücken, Outreach Top-50, Wettbewerbs-Co-Occurrence, Frühwarn-Radar, Source-Whitelist, 6-Wochen-Forecast). Voraussetzung: Atomisierung läuft (Layer 1 backfill); für Faktencheck zusätzlich Truth-Source mit human_reviewed=true.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /v1/data-mining/insights/{project} |
Voller 12-Sektionen-JSON-Payload (alle Sections in einer Response). | 0 |
| POST | /v1/data-mining/insights/{project}/refresh |
Triggert Insights-Report-Generierung (PDF + CSV-ZIP-Bundle, asynchron). HTTP 202. | 0 |
| GET | /v1/data-mining/insights/{project}/section/{section} |
Einzel-Section-Lazy-Load. {section} ∈ executive_summary, fact_errors, consensus_narratives, consensus_clusters, persona_gaps, battle_cards, outreach_top50, competitor_cooccurrence, early_warnings, source_whitelist, forecast_6w, recommended_actions. |
0 |
| GET | /v1/data-mining/exports/{project}.pdf |
Letzten generierten PDF-Export downloaden. 404 falls noch keiner existiert (vorher refresh aufrufen). |
0 |
| GET | /v1/data-mining/exports/{project}.zip |
Letztes CSV-Bundle (1 CSV pro Section, gezippt). 404 wenn nicht vorhanden. | 0 |
Alle Endpoints Sanctum-authed mit Team-Scope-Check (tracking_projects.team_id === user.currentTeam->id, sonst 403).
Wöchentlicher Insights-Report: Jeden Sonntag 18:00 wird für alle Projekte der PDF-Report generiert und mit Highlights-Summary an den Team-Owner gemailt.
Live-Dashboard: https://rankion.ai/ai-visibility/{id}/insights — interaktive 9-Tab-Ansicht des gesamten Payloads.
Layer-7-Engine über tracking_cited_sources: extrahiert pro Citation strukturierte Dimensionen (Content-Format, Schema-Types, E-E-A-T-Signale, Pricing/FAQ-Indikatoren, Domain-Authority), vergleicht gegen Google-SERP-Top-10-Counterfactuals und produziert Lift-Scores als Hebel-Empfehlungen. Drei Tiers: per Project (Tier 1), Account (Tier 2), Admin Global (Tier 3).
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /v1/citation-causality/project/{projectId}/patterns |
Alle Pattern-Rows (Dimension × Wert × Lift-Score × p-Value) für ein Projekt. Lift>1 = überproportional bei zitierten Pages. | 0 |
| GET | /v1/citation-causality/project/{projectId}/recommendations |
Priorisierte Hebel-Karten P1-P4 nach Lift × Signifikanz × Sample-Size; mit description (LLM-Klartext) und Top-3 Evidence-Citations. |
0 |
| GET | /v1/citation-causality/project/{projectId}/audit?url=<sitemap_url> |
Per-Page-Compliance-Audit: extrahiert Dimensionen der URL via Ollama, vergleicht mit Top-Patterns, liefert Gap-Liste + estimated_uplift. | 1 |
| POST | /v1/citation-causality/project/{projectId}/audit/bulk |
Bulk-Audit async für bis zu 200 URLs. Body {urls: [...]}. Returnt 202 + audit_id. Job läuft auf Queue ollama (ca. 30s pro URL). |
10 |
| GET | /v1/citation-causality/project/{projectId}/audit/bulk/{auditId} |
Status-Poll für Bulk-Audit. Returns status (pending/processing/completed/failed) + results (Array von Per-URL-Resultaten). |
0 |
| GET | /v1/citation-causality/account/cross-project |
Cross-Project-Patterns über alle Projekte des Account-Teams. Nutzt für Tier-2-Dashboard "Welcher Hebel funktioniert wo?". | 0 |
| GET | /v1/citation-causality/admin/global |
Globale Patterns über alle Customers/Branchen. Admin-only (401 sonst). Speist Tier-3-Strategie-Dashboard. | 0 |
| GET | /v1/citation-causality/admin/sandbox/runs |
Liste aller Sandbox-Runs (paginiert 25). Admin-only. Felder: name, status, total_citations_target, started_at, completed_at, estimated_cost_cents. | 0 |
| POST | /v1/citation-causality/admin/sandbox/runs |
Neuen Sandbox-Run starten. Body: {name, catalog_id, ollama_model_primary, ollama_model_fallback, prompt_template_version, citation_filter:{project_ids,limit}, serp_counterfactual_enabled, dataforseo_domain_analytics_enabled}. Returnt 202 + run_id. |
0 |
| GET | /v1/citation-causality/admin/sandbox/runs/{runId} |
Run-Detail mit verschachtelten Citations + Counterfactuals + Patterns. Admin-only. | 0 |
| POST | /v1/citation-causality/admin/sandbox/runs/{runId}/promote |
Promote eines Sandbox-Runs nach Production (Plan 3). Aktuell 501 bis Plan 3 implementiert. | 0 |
| GET | /v1/citation-causality/projects/{project}/recipe |
Plan 5 Phase E — Content-Recipe (briefing + 3-5 title_suggestions + bullets + CTA + confidence) für das Team via ContentRecipeSynthesizer. 24h Cache (key: cce.recipe.t<id>.<sha>). Bei Top-3 Hebel n_cited<5 → {available:false, reason:"insufficient_sample"}. |
0 |
| GET | /v1/citation-causality/projects/{project}/levers |
Plan 5 Phase E — {top, unique, anti} Hebel-Buckets mit Klartext-Labels (key_label, value_label) + interpretation (headline, explanation, action). Optional ?include=trend,by_llm lazy-expansion pro Lever. Bounded auf 30+20+15 Rows. |
0 |
| GET | /v1/citation-causality/projects/{project}/levers/{key}/{value}/drill-down |
Plan 5 Phase E — Per-Lever Tiefenansicht: details (cited URLs + SERP-Counterfactuals + counts), trend (Time-Axis-Verlauf), by_llm (Per-LLM-Provider Lift-Vergleich). Path-Param key regex [a-z][a-z0-9_]{0,63}. |
0 |
| POST | /v1/citation-causality/projects/{project}/audit |
Plan 5 Phase E — Dispatched AuditTopPagesAgainstHebelJob für Top-N own-domain URLs gegen Top-5 Hebel. Body optional {limit: 10} (1-50, default 10). Returnt 202 + {job, project_id, team_id, estimated_cost_credits}. Polling via Bestehender /audit/bulk/{auditId}. |
5 |
Alle Endpoints Sanctum-authed + Team-Scope (tracking_projects.team_id === user.currentTeam->id).
Skill-Caller-Idiom (für Master-Agent / Claude Code Skill / externe Agenten via REST):
POST /admin/sandbox/runs mit citation_filter:{project_ids:[N], limit:30} + serp_counterfactual_enabled:trueGET /admin/sandbox/runs/{runId} alle 30s bis status: completed (Run dauert 5-15 min)/admin/citation-causality-lab/catalog für Klon + Anpassung, dann neuer Run mit dimensions_catalog_id: <neue_id>cc_patterns + Klartext-Insights aus synthesized_text-Spalte. P1-P4-Priorisierung via /project/{projectId}/recommendations.Skill-Caller-Idiom Plan-5-Phase-E (Insight-to-Action über REST):
GET /projects/{id}/recipe — Cache-Hit liefert sofort, sonst <30s glm-4.7 CallGET /projects/{id}/levers für Top-Buckets, optional ?include=trend,by_llm für detaillierte InspectionGET /projects/{id}/levers/{key}/{value}/drill-down für Evidence (cited URLs + SERP-Counterfactuals)POST /projects/{id}/audit (5 Credits) → 202 + Job-Marker; Status via existierenden /audit/bulk/{auditId} Endpoint pollenavailable:false); Drill-Down auf bad-format Key → 404Live-Dashboards:
https://rankion.ai/ai-visibility/{projectId}?activeTab=citation-causalityhttps://rankion.ai/account/citation-intelligencehttps://rankion.ai/admin/citation-causality-intelligenceTeam-aggregiertes Markov-„Next-Route"-Modell für die intelligente Prefetch-Schicht (siehe docs/modules/turbo-prefetch.md). Team-gescopt, auth:sanctum (First-Party via Session-Cookie).
| Methode | URL | Beschreibung | Credits |
|---|---|---|---|
| POST | /v1/prefetch/transition |
Navigations-Übergang melden. Body: {from, to} (interne Pfade ^/…, kein Query/Signatur; from≠to). firstOrCreate+increment. 422 bei ungültigen Pfaden/Self-Loop. Throttle 60/min. |
0 |
| GET | /v1/prefetch/predictions?from=&limit= |
Top-N team-aggregierte Folge-Routen für from. Returns {from, predictions:[{to_route, count, prob}]} (prob = count/Σcount, sortiert desc). limit 1–10 (Default 5). |
0 |
| GET | /v1/prefetch/config |
Client-Config: {enabled, weights, limit, prerender_allowlist}. |
0 |
Workflow: nach jeder Navigation transition melden (fire-and-forget) → beim Einstieg predictions?from=<pfad> lesen → client-seitig mit lokalem localStorage-Markov blenden (lokal führend, Server = Cold-Start). Kein Polling, keine Job-Lifecycles.
User-scoped (NICHT team-scoped) — Konto-Sicherheitsstatus lesen, Passwort ändern, andere Sitzungen beenden. auth:sanctum, gelöst über den authentifizierten User ($request->user()), kein Team-Scope.
| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | /account |
Sicherheitsstatus: {id, name, email, email_verified, two_factor_enabled, auth_provider, pending_email}. Enthält NIEMALS Secrets (kein two_factor_secret, keine recovery_codes, kein pending_email_token). |
— |
| PUT | /account/password |
Passwort ändern. Body: {current_password, password, password_confirmation}. current_password ist serverseitig Pflicht (422 sonst) — ein Token allein reicht NICHT. Google-Konten → 422 (Passwort per Reset-Link). throttle:10,1. |
— |
| DELETE | /account/sessions |
Beendet alle anderen Sitzungen des Users (löscht nur dessen eigene sessions-Rows außer der aktuellen). Response: {message, deleted}. throttle:10,1. |
— |
| GET | /account/notification-preferences |
Benachrichtigungs-Präferenz-Matrix (Default-aufgelöst): {data:{email:{reports,tracking_alerts,job_results}, inapp:{…}, locked:{billing,security,team}}}. email/inapp = abbestellbare Kategorien als bool (Default true); locked = immer-aktive Kategorien. |
— |
| PUT | /account/notification-preferences |
Präferenzen setzen. Body: {email?:{reports?:bool, tracking_alerts?:bool, job_results?:bool}, inapp?:{…}}. NUR optionale Kategorien werden berücksichtigt (nicht-optionale billing/security/team werden ignoriert). Es werden NUR Abweichungen (false) persistiert; alles-an ⇒ NULL. Antwort = GET-Shape (round-trip). |
— |
2FA-Setup/-Deaktivierung, E-Mail-Änderung und Konto-Löschung sind absichtlich nur in der Web-Oberfläche verfügbar, nicht über die REST-API. Begründung: Ein geleakter Sanctum-Token darf weder 2FA abschalten noch die E-Mail-Adresse übernehmen noch das Konto löschen können. Die Passwort-Änderung ist die einzige sicherheitskritische Schreib-Operation per API und verlangt deshalb zusätzlich das aktuelle Passwort (current_password).
Use-Case: Programmatischer Self-Service-Check + Härtung des eigenen Kontos (Status lesen, Passwort rotieren, fremde Sessions kappen) ohne Web-UI.
Idiom: GET /account (Status lesen — two_factor_enabled/email_verified prüfen) → bei Bedarf PUT /account/password (mit current_password + neuem Passwort, 422 bei falschem aktuellem PW) → optional DELETE /account/sessions (nach Passwortwechsel andere Geräte abmelden). Single-Resource (kein Bulk, kein Polling). Für Benachrichtigungen: GET /account/notification-preferences (volle Matrix lesen) → PUT /account/notification-preferences (nur die abzuschaltenden optionalen Kategorien als false senden; Default-an muss nicht mitgeschickt werden) → erneutes GET bestätigt den Round-trip.
Anti-Patterns: Kein Versuch, 2FA/E-Mail/Löschung per API zu fahren — diese Endpoints existieren bewusst nicht (siehe Sicherheits-Ausnahme). Passwort-PUT nicht ohne current_password aufrufen (deterministisch 422). DELETE /sessions löscht nie fremde User-Rows. Beim Notification-PUT nicht billing/security/team abzuschalten versuchen — diese werden serverseitig ignoriert (kein 422, aber kein Effekt); locked ist read-only.
Cross-Module-Bezüge: Token-Erstellung/-Widerruf unter /settings/api-tokens (Web). Rechnungs-/Team-Verwaltung → ## 🧾 Rechnungsdaten bzw. Team-Members-API.