# Rankion.ai API Reference

> **Stand:** 2026-05-12 · **Version:** v1 · **343 Endpoints**

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

## Authentifizierung

```bash
# Token im Frontend erstellen: /settings/api-tokens
TOKEN="<dein-sanctum-token>"
BASE="https://rankion.ai/api/v1"

curl -H "Authorization: Bearer $TOKEN" \
     -H "Accept: application/json" \
     "$BASE/projects"
```

Alle Endpoints sind Team-scoped via `auth:sanctum` Middleware. Cross-Team-Zugriff → `403`.

## Antwort-Formate

- **2xx** Success — JSON
- **202 Accepted** — Job dispatched (async). Response enthält `keyword_id`, `article_id` etc. zum Pollen.
- **400/422** Validation Error — `{message, errors:{field:[reason]}}`
- **403** IDOR / Team-Mismatch
- **402** Insufficient credits
- **404** Resource not found
- **409** Conflict (z.B. Article bereits am publishen)

## Credit-Mechanik

Jobs/Aktionen mit `credits:N` Middleware ziehen N Credits aus dem Team-Balance vor Job-Dispatch. Bei `cache=false` Refresh (Explorer) wird inline 50 Credits abgebucht.

---

## 🎯 AI Visibility Tracking

### Projekte

| Method | URL | Beschreibung | Credits |
|--------|-----|--------------|---------|
| GET    | `/tracking-projects` | alle Team-Projekte | — |
| POST   | `/tracking-projects/analyze` | Wizard Step 1+2: Domain/Keyword analysieren, dispatcht `AnalyzeDomainJob`. Body: `{mode:"domain"\|"keyword", domain?, keyword?, name?, language?, country?}` → 202 + `project_id` | — |
| GET    | `/tracking-projects/{id}` | Detail mit KPIs | — |
| PUT    | `/tracking-projects/{id}` | Settings: `{name?, brand_name?, brand_aliases[], tracking_frequency?, llm_platforms[], llm_with_search?, llm_without_search?, track_aio?, reality_check_enabled?}` | — |
| 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[]}` | — |
| 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 | — |

### Keywords / Prompts

| Method | URL | Notes |
|--------|-----|-------|
| GET    | `/tracking-projects/{id}/keywords` | `?active_only=true` |
| POST   | `/tracking-projects/{id}/keywords` | Body: `{keyword, language?, country?, is_active?}` |
| PUT    | `/tracking-keywords/{id}` | |
| DELETE | `/tracking-keywords/{id}` | |
| GET    | `/tracking-projects/{id}/prompts` | |
| POST   | `/tracking-projects/{id}/prompts` | Body: `{prompt_text, prompt_category?, persona_label?, commercial_intent?}` |
| PUT    | `/tracking-prompts/{id}` | |
| DELETE | `/tracking-prompts/{id}` | |
| POST   | `/tracking-projects/{id}/prompts/import` | CSV/Excel Bulk-Import. multipart/form-data: `file`, optional `mapping[col_name]=index`. → `{imported, skipped, errors, mapping_used}` |

### Cited Sources

| Method | URL | Notes |
|--------|-----|-------|
| GET    | `/tracking-projects/{id}/cited-sources` | `?domain=&outreach_status=&is_own_domain=` |
| GET    | `/tracking-projects/{id}/cited-sources/export` | CSV-Stream. `?from=&to=&status=&platform=&search=&sort=` |
| POST   | `/tracking-projects/{id}/cited-sources/analyze` | Citation-Analyse dispatchen. Body optional `{source_ids:[]}`. Default: alle unanalyzed. → 202 + `{queued, estimated_seconds}` |
| PATCH  | `/tracking-projects/{id}/cited-sources/{source_id}` | Body: `{outreach_status?:neu\|anfrage_gestellt\|verlinkt\|abgelehnt\|ignoriert, notes?}` |

### Insights

| Method | URL | Notes |
|--------|-----|-------|
| GET    | `/tracking-projects/{id}/scores` | Score-Historie (`?from=&to=`) |
| GET    | `/tracking-projects/{id}/platform-breakdown` | (`?from=&to=`) |
| GET    | `/tracking-projects/{id}/insights/low-hanging-fruit` | (`?days=30&min_possibility=60&limit=5`) |
| GET    | `/tracking-projects/{id}/insights/platform-gap` | (`?days=30`) |
| POST   | `/tracking-projects/{id}/insights/platform-gap/{platform}/diagnose` | async — 1 Credit |
| GET    | `/tracking-projects/{id}/insights/potential-hero` | `{eligible:false}` wenn <20 Abfragen |
| POST   | `/tracking-projects/{id}/prompts/{prompt}/generate-brief` | Content-Brief via Claude — 3 Credits |
| POST   | `/tracking-projects/{id}/brand-aliases` | Body: `{alias}` |

### Reality Check (AI Visibility Index)

| Method | URL | Notes |
|--------|-----|-------|
| GET    | `/tracking-projects/{id}/avi` | neuester AVI-Score, tier (red/yellow/green), 6-Dim-Scores, recommendations. 404 wenn kein Run |
| GET    | `/tracking-projects/{id}/dimensions` | alle 6 Dimension-Scores pro Plattform |
| POST   | `/tracking-projects/{id}/reality-check-report` | PDF-Generierung async. 404 wenn kein Run |
| GET    | `/tracking-projects/{id}/reality-check-report/status` | Polling-Endpoint für die laufende Report-Generierung: `{status: queued\|running\|ready\|failed, progress?, download_url?}` |

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

User-Prompt geht parallel an ausgewählte LLMs (ChatGPT, Perplexity, Claude, Gemini, Copilot, Grok) je mit/ohne Web-Search. Live-Card-Grid in der UI, persistente Runs, Sentiment + Brand-Extract pro Antwort.

| Method | URL | Beschreibung | Credits |
|--------|-----|--------------|---------|
| GET    | `/prompt-check` | Liste runs des Teams. `?project_id=&status=&per_page=&page=` | — |
| POST   | `/prompt-check` | Neuer Run async (202). Body: `{tracking_project_id, prompt (5-5000ch), platforms:["chatgpt","claude",…], with_search?:{platform:bool}, title?}` | 1 pro Platform×Mode |
| GET    | `/prompt-check/{id}` | Detail: run + alle responses mit response_text, sentiment, brands_mentioned, citations | — |
| POST   | `/prompt-check/{id}/rerun` | Re-Run gleicher Settings, neue run-id (202) | 1 pro Platform×Mode |
| GET    | `/prompt-check/{id}/export` | Download `?format=csv\|json` | — |
| DELETE | `/prompt-check/{id}` | Soft-delete | — |

**Status-Lifecycle:** `pending → running → completed | partial | failed`. Polling alle 3-5s.
**Web-Search-Support:** chatgpt, perplexity, claude, gemini. Copilot/Grok ignorieren das Flag.
**Skills:** `prompt-check.{list-runs,get-run,create-run,export-run}` — Chatbot-fähig via Agentic-Skill-Index.

---

## 📝 Articles

### CRUD

| Method | URL | Notes |
|--------|-----|-------|
| GET    | `/projects/{project}/articles` | paginated 20 |
| POST   | `/projects/{project}/articles` | Body: `{title, slug?, content?, status?}` |
| GET    | `/articles/{id}` | full payload |
| PUT    | `/articles/{id}` | Body: `{title?, slug?, content?, status?, meta_description?}` |
| DELETE | `/articles/{id}` | |

### Aktionen

| Method | URL | Credits |
|--------|-----|---------|
| POST   | `/articles/{id}/score` | — |
| POST   | `/articles/{id}/optimize` | — |
| POST   | `/articles/{id}/generate` — Body: `{keyword, article_type?, outline?, target_length?, tone?, language?, style_profile_id?, content_goal_id?}` | 5 |
| POST   | `/articles/{id}/publish` — Body: `{cms_integration_id}`. 409 wenn bereits in flight | — |
| POST   | `/articles/{id}/repurpose` — Body optional `{format:linkedin\|twitter\|instagram\|newsletter\|youtube_script\|tiktok_script\|facebook}`. Ohne format → alle | 3 |

### Versions

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

### Freshness

| Method | URL |
|--------|-----|
| GET    | `/articles/{id}/freshness` |
| POST   | `/articles/{id}/freshness/check` |
| GET    | `/articles/{id}/freshness/history` |

### Internal Linking

| Method | URL |
|--------|-----|
| GET    | `/articles/{id}/link-suggestions` |
| POST   | `/projects/{project}/internal-links/analyze` (5 Credits) |
| PUT    | `/link-suggestions/{id}` |

### Standalone

| Method | URL | Credits |
|--------|-----|---------|
| POST   | `/generate/article` | 5 |
| POST   | `/generate/image` | 5 |

---

## 🔍 SEO Suite

### Rank Tracker (Phase 1)

| Method | URL |
|--------|-----|
| GET    | `/rank-tracker` |
| GET    | `/rank-tracker/{project}` |
| GET    | `/rank-tracker/{project}/keywords` (`?device=&country=&is_active=`) |
| POST   | `/rank-tracker/{project}/keywords` Body: `{keywords[], devices[]}` async 202 |
| POST   | `/rank-tracker/{project}/refresh` |
| GET    | `/rank-tracker/keywords/{keyword}/history` (30d/90d/365d) |

### Keyword Explorer (Phase 2)

| Method | URL | Credits |
|--------|-----|---------|
| POST   | `/explorer` Body: `{seed, language?, country?, cache?}`. `cache=false` umgeht 60-min-Cache | 50 |
| GET    | `/explorer/{keyword}` Volume + KD + Trends + SERP. `?include=related,questions` inlined die Listen. `?cache=false` erzwingt fresh fetch | 0 / 50* |
| GET    | `/explorer/{keyword}/related` `?type=related\|question`. `?cache=false` | 0 / 50* |

### Keyword Gap (Phase 3)

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

### Site Audit (Phase 4)

| Method | URL |
|--------|-----|
| GET/POST | `/site-audit` |
| GET    | `/site-audit/{crawl}` · `.../issues` |
| PATCH  | `/site-audit/{crawl}/issues/bulk` |
| PATCH  | `/site-audit/issues/{issue}` |
| POST   | `/site-audit/issues/{issue}/generate-brief` |

**Issues — response fields.** Each item in `GET /site-audit/{crawl}/issues` returns:
`{id, site_crawl_id, page_id, url, issue_type, severity, message, context, fix_priority, status, has_ai_brief, ai_brief_generated_at}`. The `url` field is sourced from the crawl page (eager-loaded) and is `null` if the page row was deleted.

**Bulk-mark — `PATCH /site-audit/{crawl}/issues/bulk`.** Throttle: 30/min. Body:
```json
{
  "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:{...}}}`.

**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 name
- `grounding_v1_5_gp_lead_single_sentence_definition` — lead is not a 1-sentence definition
- `grounding_v1_5_gp_lead_retrieval_sentence` — missing retrieval-stabilization sentence
- `grounding_v1_5_gp_h2_entity_prefix`, `grounding_v1_5_gp_disambiguation_block`
- `grounding_v1_5_gp_human_notice`, `grounding_v1_5_gp_timestamps_visible`
- `grounding_v1_5_gp_anchors_stable_ids`, `grounding_v1_5_gp_facts_dl_grid`
- `grounding_v1_5_gp_jsonld_present`, `grounding_v1_5_gp_jsonld_faq_when_html_faq`
- `grounding_v1_5_gp_head_canonical_absolute`, `grounding_v1_5_gp_eeat_author_bio_schema`
- `grounding_v1_5_gp_faq_entity_repeated`, `grounding_v1_5_gp_volatile_dated_sourced`

Filter via `GET .../issues?issue_type=grounding_v1_5_gp_h1_entity_only`. Bulk-mark via `filter:{issue_type:"grounding_v1_5_gp_eeat_author_bio_schema"}`.

### Backlinks (Phase 5 + Phase 6 DFS-Erweiterungen)

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

### Backlinks by Domain (one-shot lookup, ohne Projekt-Setup)

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

### Backlinks Subscription

| Method | URL | Beschreibung |
|--------|-----|--------------|
| GET    | `/teams/current/backlinks-subscription` | Aktive Subscription des Teams (Tier, Limits, Renewal) |
| POST   | `/admin/teams/{team}/backlinks-subscription` | Admin-Override: Subscription für anderes Team setzen (Body: `{tier, limits?}`) |

---

## 🛠 Keywords (Research & Expansion)

| Method | URL | Credits |
|--------|-----|---------|
| GET    | `/projects/{project}/keywords` | — |
| POST   | `/projects/{project}/keywords` | — |
| DELETE | `/keywords/{id}` | — |
| POST   | `/keywords/research` | 5 |
| POST   | `/keywords/{id}/expand` | 10 |
| GET    | `/keywords/{id}/expansions` | — |

---

## ✍️ Content Intelligence

### Storylines

| Method | URL |
|--------|-----|
| GET/POST | `/projects/{project}/storylines` |
| GET/PUT/DELETE | `/storylines/{id}` |

### Style Profiles

| Method | URL | Beschreibung | Credits |
|--------|-----|--------------|---------|
| GET    | `/projects/{project}/style-profiles` | Auflisten (Response enthält zusätzlich `source_type`, `source_url`, `pending_review`, `scraped_pages`, `failure_reason`, `reviewed_at`) | — |
| POST   | `/projects/{project}/style-profiles` | Manuell erstellen | — |
| GET    | `/style-profiles/{id}` | Detail (gleiche Zusatzfelder) | — |
| PUT    | `/style-profiles/{id}` | Aktualisieren. Wenn `pending_review=true`, wird das Flag implizit auf `false` gesetzt und `reviewed_at=now()` belegt (Detail-Edit zählt als Review-Akzeptanz). | — |
| DELETE | `/style-profiles/{id}` | Löschen | — |
| POST   | `/projects/{project}/style-profiles/from-url` | URL-basierte Stilanalyse als Background-Job starten. ScraperAPI lädt bis zu 10 inhaltsstarke Seiten, Claude erstellt das Profil. Fertiges Profil hat `pending_review=true`. HTTP 202 → `{ data: { id, status: "pending", source_url, message } }` | 25 |
| GET    | `/style-profiles/{id}/scrape-status` | Polling-Endpoint für laufenden Background-Job. Liefert `{ status, source_url, scraped_pages, failure_reason, pending_review }` | — |
| POST   | `/style-profiles/{id}/accept` | Profil akzeptieren: setzt `pending_review=false`, `reviewed_at=now()`. Response: `{ data: <StyleProfile> }` | — |
| POST   | `/style-profiles/{id}/discard` | Profil verwerfen (Soft-Delete). Nur wenn `pending_review=true`, sonst 422. Response: `{ deleted: true }` | — |

### Knowledge Base

| Method | URL | Beschreibung | Credits |
|--------|-----|--------------|---------|
| GET/POST | `/projects/{project}/knowledge-base` | Docs auflisten / hochladen | — |
| DELETE | `/knowledge-base/{id}` | Doc löschen | — |
| POST | `/knowledge-base/url` | URL ingestieren (async) | 5 |

### Link Lists

| Method | URL |
|--------|-----|
| GET/POST | `/projects/{project}/link-lists` |
| DELETE | `/link-lists/{id}` |

### Orphan Scans

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

### Overview

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

---

## 📅 Calendar & Images

### Calendar

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

### Project Images (legacy per-project listing)

| Method | URL | Beschreibung |
|--------|-----|--------------|
| GET    | `/projects/{project}/images` | Listet alle Bilder eines Projekts (legacy — die team-weite Gallery `/v1/images` unten ist der primäre Pfad) |

### Image Gallery (permanent, team-scoped)

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`, `?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 `/v1/images/edits`. 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) |

---

## 🤖 AI Tools

### Agentic Chat — Master Agent als Endpoint

| 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 Sprache
- `session_id` (int, optional): bestehende Session fortführen

Query-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:
- Synchron — komplette Loops können 5–10 min dauern (PHP-FPM 600 s Hardlimit, Client-Timeout entsprechend setzen)
- Triple-scoped (team_id + user_id + id) gegen IDOR
- Spart externen Clients die Orchestrierung der 12+ Meta-Tools — der Master-Agent wählt + ruft selbst auf
- Iterations-Cap 150, Wall-Clock-Cap 25 min, No-Progress-Detector 8 — alle env-overridable, alle münden bei Hit in einen tools-disabled Summarize-Call statt einer harten Fehlermeldung
- Companion Claude Code Skill: `rankion-agent`

### AI Scanner / Detection

| Method | URL | Credits |
|--------|-----|---------|
| POST   | `/ai-scanner/detect` | 2 |
| POST   | `/ai-scanner/humanize` | 5 |

### Humanizer

| Method | URL | Credits |
|--------|-----|---------|
| POST   | `/humanize` | 8 |
| GET    | `/humanize/{batch}` · `.../documents/{document}` | — |

### Content Optimizer

| Method | URL | Credits |
|--------|-----|---------|
| GET    | `/content-optimizer` · `/content-optimizer/{id}` · `/content-optimizer/optimizations/{id}` (alias) | — |
| POST   | `/content-optimizer/analyze` | 5 |
| POST   | `/content-optimizer/{id}/apply` · `/content-optimizer/optimizations/{id}/apply` (alias) | 5 |

**Async-Vertrag (Subsystem B P0.3 — fixed 2026-05-09):**

`POST /v1/content-optimizer/analyze` returns `202` mit:
```json
{
  "message": "Content optimization started",
  "id": 6,
  "optimization_id": 6,
  "poll_url": "/api/v1/content-optimizer/6"
}
```
Plus Header `Location: /api/v1/content-optimizer/6`.

**Polling-Pfad:** `GET /api/v1/content-optimizer/6` (canonical) — der Pfad `/optimizations/6` funktioniert auch (Alias-Route, weil das Field heisst `optimization_id`). Caller können beides.

`optimization_id` bleibt für Kompatibilität, aber neue Caller sollten `id` lesen.

---

## 🥊 Competitor Analysis

| Method | URL | Credits |
|--------|-----|---------|
| GET/POST | `/competitor-analyses` | 20 (POST) |
| GET    | `/competitor-analyses/{id}` · `.../gaps` | — |
| POST   | `/competitor-analyses/{id}/rerank-competitors` | — |

---

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

Site-wide content-quality crawler. Walks a domain (sitemap-discovered), scores each page on SEO (0–100), content quality (0–100), word count, and emits `issues[] = {type:error|warning|info, category, message}` plus `recommendations[]`. Distinct from `/site-audit` — this module is the *content-quality* angle, `/site-audit` is the *crawl/issue/grounding* angle.

| Method | URL | Credits |
|--------|-----|---------|
| GET/POST | `/content-audits` | 10 (POST) |
| GET    | `/content-audits/{id}` · `.../export` · `.../pages` | — |
| PATCH  | `/content-audits/{id}/pages/bulk` | — |
| GET    | `/content-audit-pages/{id}` | — |
| PATCH  | `/content-audit-pages/{id}/solved` | — |

**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|failed`
- `type=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|low`
- `min_seo_score=0..100`, `max_seo_score=0..100`
- `issue_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, …}}`.

**Bulk-mark — `PATCH /content-audits/{id}/pages/bulk`.** Throttle: 30/min. Body:
```json
{
  "filter": {
    "priority": "low",
    "status": "open",
    "min_seo_score": 0,
    "max_seo_score": 50,
    "issue_type": "error"
  },
  "is_solved": true
}
```
All `filter.*` fields are optional. `filter.status` is the FROM-state to match (only flips rows currently in this state). `is_solved` is required and is the new state to write. Response: `{data:{affected:int, is_solved:bool}, meta:{applied_filter:{…}}}`. Team-scoped via `audit.team_id` — cross-team access returns 403.

**`filter.project_id` → 422** (Variante 1 Rename 2026-05-10): Pages haben keinen Project-FK; das Projekt ergibt sich aus dem Parent-Audit. Vorher wurde `filter.project_id` stumm gedroppt — was Mass-Updates über alle Pages des Audits zog statt nur über die eines Projekts. Jetzt 422 mit klarem Hint.

---

## 👥 Community Monitor

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

---

## 📈 Site Monitor

| Method | URL |
|--------|-----|
| GET/POST | `/site-monitor` |
| GET/PUT/DELETE | `/site-monitor/{id}` |
| POST   | `/site-monitor/{id}/pause` · `.../resume` · `.../check-now` |
| GET    | `/site-monitor/{id}/checks` · `.../incidents` · `.../lighthouse` |

---

## 🚀 Automation

### Bulk Generations

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

### Autopilot

| Method | URL |
|--------|-----|
| GET/POST | `/autopilot` Body: `{module, frequency, config}` |
| PUT/DELETE | `/autopilot/{id}` |

### Pipelines

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

---

## 🏗 Projects & Settings

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

---

## 🎯 Action Center

| Method | URL |
|--------|-----|
| GET/POST/PATCH | `/action-center/goal-profile` |
| GET    | `/action-center/goal-templates` · `.../industry-templates` · `.../seo-detectors` |
| GET    | `/action-center/path` · `/action-center/paths` |
| POST   | `/action-center/path/recompute` |
| GET/POST | `/action-center/todos` |
| GET/DELETE | `/action-center/todos/{id}` |
| POST   | `/action-center/todos/{id}/complete` · `.../snooze` |

---

## 🔌 Google Integrations

| Method | URL | Credits |
|--------|-----|---------|
| GET    | `/google/connections` | — |
| DELETE | `/google/connections/{id}` | — |
| GET    | `/google/gsc/properties` | — |
| POST/DELETE | `/google/gsc/properties/{p}/link` | — |
| POST   | `/google/gsc/properties/{p}/sync` | 1 |
| GET    | `/google/gsc/properties/{p}/metrics` | — |
| GET    | `/google/ga4/properties` | — |
| POST/DELETE | `/google/ga4/properties/{p}/link` | — |
| GET    | `/google/ga4/properties/{p}/metrics` | — |
| GET    | `/integrations/google/reviews/status` | — |

---

## ⭐ Review Sources

| Method | URL |
|--------|-----|
| GET    | `/review-sources` · `/review-sources/{id}` |
| DELETE | `/review-sources/{id}` |
| POST   | `/review-sources/{id}/rotate-key` · `.../disconnect` · `.../reconnect` |
| POST   | `/review-sources/webhook` · `/review-sources/waitlist` |
| POST   | `/webhooks/reviews/{source}` *(public, key-auth)* |

---

## 📰 Blog (Admin)

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

---

## 💾 Rankion OS

| Method | URL |
|--------|-----|
| PUT    | `/os/mode` |
| GET/POST | `/os/preferences` |
| GET/POST | `/os/files` |
| GET/PUT/DELETE | `/os/files/{id}` |
| POST   | `/os/files/{id}/move` · `.../share` |
| DELETE | `/os/files/{id}/share` |
| GET/POST | `/os/folders` |
| GET/PUT/DELETE | `/os/folders/{id}` |
| POST   | `/os/folders/{id}/move` |
| GET    | `/os/desktop` · `/os/spotlight` |

---

## 💳 Account / Meta

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

---

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

Auditiert eine einzelne Landingpage: 3 Source-Screenshots (Desktop/Tablet/Mobile mit echter UA-Emulation), Lighthouse-Metriken, Opus 4.7 Multimodal-Vision-Analyse plus bis zu 3 gpt-image-2 KI-Renders der optimierten Variante (Desktop ist die Design-Basis, Tablet+Mobile sind responsive Adaptionen). Authentifizierung: `Authorization: Bearer <token>`, alle Endpoints team-scoped.

| Method | URL | Credits | Beschreibung |
|--------|-----|---------|--------------|
| POST   | `/page-audit` | 30 + bis zu 3×15 | Audit starten. Body: `{url, tracking_project_id?, persona?}` → 202 + `{id, status:"pending", url}` |
| GET    | `/page-audit/{id}` | — | Detail mit 6 Bild-URLs + `opus_analysis` + Lighthouse |
| GET    | `/page-audits` | — | Paginierte Liste. `?per_page=25&tracking_project_id=` |

**Pipeline-Status:** `pending` → `scraping` → `screenshotting` → `analyzing` → `completed`. KI-Renders laufen in einem separaten Background-Job nach `completed`; die Felder `ideal_screenshot_url`/`ideal_tablet_url`/`ideal_mobile_url` füllen sich nach und nach (Desktop zuerst, dann Tablet, dann Mobile).

**Polling-Pattern:**

```bash
# 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 (Hauptflow ~1-2 min, KI-Renders +6-9 min)
while true; do
  R=$(curl -s -H "Authorization: Bearer $TOKEN" $BASE/page-audit/$ID)
  STATUS=$(echo "$R" | jq -r '.status')
  IDEAL_M=$(echo "$R" | jq -r '.ideal_mobile_url // "null"')
  echo "$STATUS mobile=$([ "$IDEAL_M" = null ] && echo no || echo yes)"
  [ "$STATUS" = completed ] && [ "$IDEAL_M" != null ] && break
  sleep 30
done
```

**Response-Shape (`GET /page-audit/{id}` nach `completed`):**

```json
{
  "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",
  "screenshot_tablet_url":  "https://rankion.ai/storage/page-audits/10/screenshot-tablet.png",
  "screenshot_mobile_url":  "https://rankion.ai/storage/page-audits/10/screenshot-mobile.png",
  "ideal_screenshot_url":   "https://rankion.ai/storage/page-audits/10/ideal.png",
  "ideal_tablet_url":       "https://rankion.ai/storage/page-audits/10/ideal-tablet.png",
  "ideal_mobile_url":       "https://rankion.ai/storage/page-audits/10/ideal-mobile.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:**
- **Iterative Optimierung:** `analysis.improvement_suggestions` (sortiert nach `priority`) lesen → Änderungen am Source-Page anwenden → Re-Audit → Score-Diff vergleichen.
- **A/B Variant-Generation:** `ideal_screenshot_url` (Desktop+Mobile) als Visual-Reference in den Design-Pipeline-Iteration füttern.
- **Persona-driven Copy:** `analysis.personas[].pain_points` + `goals` → gezielte Headline-/CTA-Varianten schreiben → re-audit.
- **Headline-Drop-In:** `analysis.headline_rewrite` ist ein validierter Headline-Replacement — kann direkt in den Live-Page übernommen werden.

---

## 📑 Reports & Cross-Module Correlation

Auto-Generator für Tracking-Project-Reports (Markdown-Output via `GenerateTrackingProjectReportJob`) und ein Cross-Modul-Korrelations-Endpoint, der Site-Audit, Rank-Tracker, Backlinks und AVI verknüpft. Authentifizierung: `Authorization: Bearer <token>`, alle Endpoints team-scoped.

### Reports

| Method | URL | Credits | Beschreibung |
|--------|-----|---------|--------------|
| POST   | `/tracking-projects/{id}/generate-report` | 10 | Report generieren (async). 202 + `{report_id, tracking_project_id, status, status_endpoint}`. **Rate-Limit:** 1 Request / 30 Min / Project — sonst 429 `{error:"rate_limited"}`. |
| GET    | `/tracking-projects/{id}/reports` | — | Letzte 50 Reports des Projects (sortiert nach `created_at desc`). |
| GET    | `/reports/{id}` | — | Report-Detail inkl. `markdown_content` und `summary_json`. |

**Status-Werte:** `pending` → `generating` → `completed` (oder `failed`).

**Polling-Pattern:**

```bash
# 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):**

```json
{
  "report_id": 42,
  "tracking_project_id": 7,
  "status": "pending",
  "status_endpoint": "/v1/reports/42",
  "message": "Report dispatched"
}
```

**Response — `GET /tracking-projects/{id}/reports` (200):**

```json
{
  "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):**

```json
{
  "id": 42,
  "tracking_project_id": 7,
  "team_id": 3,
  "user_id": 12,
  "status": "completed",
  "markdown_content": "# Tracking-Report Project XY\n\n## Executive Summary\n…",
  "summary_json": {
    "headline_kpis": { "avi_score": 64, "delta_30d": "+8", "top10_keywords": 14 },
    "wins": [ "..." ],
    "risks": [ "..." ]
  },
  "credits_used": 10,
  "error_message": null,
  "generated_at": "2026-04-27T18:14:22+00:00",
  "created_at": "2026-04-27T18:13:40+00:00",
  "updated_at": "2026-04-27T18:14:22+00:00"
}
```

**Status-Codes:** 202 (Report dispatched), 200 (List/Detail), 403 (Cross-Team / Cross-Project), 404 (Report/Project nicht gefunden), 429 (Rate-Limit: max 1 / 30 Min).

### Cross-Module Correlation

Aggregiert drei Korrelations-Datenpunkte für einen TrackingProject in einem Roundtrip — Risk-Map (Site-Audit × Rank-Tracker), Backlinks-Velocity × AVI-Trend (Pearson, 30d), Smart-Todos.

| Method | URL | Credits | Beschreibung |
|--------|-----|---------|--------------|
| GET    | `/tracking-projects/{id}/correlation` | — | Cross-Module-Korrelation Snapshot |

**Response — `GET /tracking-projects/{id}/correlation` (200):**

```json
{
  "data": {
    "site_audit_ranking_risks": [
      {
        "keyword_id": 91,
        "keyword": "best crm",
        "search_volume": 4400,
        "position": 5,
        "url": "https://example.com/best-crm",
        "issues_count": 7,
        "critical_count": 2,
        "medium_count": 3,
        "risk_score": 8.4,
        "issues": [
          { "type": "missing_h1", "severity": "critical", "message": "..." }
        ]
      }
    ],
    "backlinks_av_correlation": {
      "correlation_coefficient": 0.612,
      "days_with_data": 14,
      "velocity_total_30d": 23,
      "avi_data_points_30d": 18,
      "backlink_events_30d": 41,
      "observation": "Moderat positive Korrelation: Backlink-Velocity beeinflusst AVI."
    },
    "smart_todos": [
      {
        "priority_score": 84,
        "estimated_impact": "medium",
        "title": "Page-Issues fixen die Top-Ranking gefährden",
        "why": "URL …/best-crm rankt für \"best crm\" auf Position 5 — hat aber 7 Site-Audit-Issues (missing_h1, ...).",
        "reference_type": "tracking_keyword",
        "reference_id": 91,
        "url": "https://example.com/best-crm",
        "keyword": "best crm",
        "position": 5,
        "risk_score": 8.4
      }
    ]
  }
}
```

**Status-Codes:** 200 (Erfolg), 403 (Cross-Team / Cross-Project), 404 (Project nicht gefunden).

---

## Beispiele (curl)

### Wizard-Flow komplett über API

```bash
# 1) Analyse starten
RESP=$(curl -s -X POST $BASE/tracking-projects/analyze \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"mode":"domain","domain":"example.com","language":"de","country":"DE"}')
PID=$(echo $RESP | jq -r .project_id)

# 2) Polling bis completed
while [ "$(curl -s $BASE/tracking-projects/$PID/analysis-status \
  -H "Authorization: Bearer $TOKEN" | jq -r .analysis_status)" != "completed" ]; do
  sleep 3
done

# 3) Atomic Aktivierung
curl -X POST $BASE/tracking-projects/$PID/activate \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "keywords":["best crm","crm software"],
    "competitors":[{"domain":"hubspot.com"}],
    "prompts":[{"prompt":"What is the best CRM?","category":"recommendation"}],
    "platforms":["chatgpt","perplexity"],
    "frequency":"weekly",
    "with_search":true,
    "reality_check_enabled":true
  }'
```

### Keyword Explorer mit allen Daten

```bash
curl "$BASE/explorer/176007?include=related,questions" \
  -H "Authorization: Bearer $TOKEN"
# returns: keyword, search_volume, kd_score, parent_topic, traffic_potential,
# serp_urls, monthly_searches[12], related[90], questions[11], cache_hit
```

### Article generieren + publishen

```bash
# Generate
curl -X POST $BASE/articles/$ART/generate \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"keyword":"AI coding tools","article_type":"blog","target_length":1500}'

# Publish to WordPress (CMS-ID 1 vorausgesetzt)
curl -X POST $BASE/articles/$ART/publish \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"cms_integration_id":1}'
```

### Cited-Sources Outreach

```bash
# Liste mit Filter
curl "$BASE/tracking-projects/$PID/cited-sources?outreach_status=neu" \
  -H "Authorization: Bearer $TOKEN"

# Status updaten
curl -X PATCH $BASE/tracking-projects/$PID/cited-sources/$SID \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"outreach_status":"anfrage_gestellt","notes":"E-Mail an redaktion@..."}'

# CSV-Export
curl "$BASE/tracking-projects/$PID/cited-sources/export?from=2026-01-01" \
  -H "Authorization: Bearer $TOKEN" -o sources.csv
```

---

## Konventionen

- **Async Jobs:** Endpoints mit `credits:N` und/oder Job-Dispatch geben `202 Accepted` mit Job-ID. Polling via Detail-Endpoint bis `processing_status == 'ready'`.
- **Cache-Bust:** Endpoints die intern cachen unterstützen `?cache=false` (z.B. Explorer). Inline 50 Credits Charge.
- **Filter via Query:** `?from=&to=&status=&platform=&search=&sort=&limit=&page=`.
- **Pagination:** `?per_page=N&page=N`. Response: `{data:[], meta:{total, per_page, current_page, last_page}}`.
- **Includes:** Wo unterstützt: `?include=related,questions` (kommagetrennt) inlined Sub-Resources statt separater Roundtrips.

## Routes-Generierung

Diese Datei spiegelt `routes/api.php`. Bei Code-Änderungen Route-Liste neu prüfen:

```bash
php artisan route:list | grep "api/v1" | wc -l
```

## Frontend-Doku-Link

Live-Tabellen mit Beispielen: https://rankion.ai/settings/api-documentation

---

## Chat Shares

Public-share API for AI chat sessions. Owner creates a share link from `/api/v1/chat-shares`, recipient views it via `/share/{token}` (public, noindex). All endpoints require `auth:sanctum` and team-scoping.

| Method | URL | Description | Credits |
|---|---|---|---|
| GET | `/v1/chat-shares` | List own share links (paginated, 25/page) | 0 |
| POST | `/v1/chat-shares` | Create new share for owned session (default 7-day expiry) | 0 |
| GET | `/v1/chat-shares/{id}` | Detail + stats | 0 |
| PATCH | `/v1/chat-shares/{id}` | Update settings | 0 |
| DELETE | `/v1/chat-shares/{id}` | Revoke (soft delete; row preserved) | 0 |
| GET | `/v1/chat-shares/{id}/views` | Paginated view log (DSGVO-anonymized) | 0 |
| GET | `/v1/agentic/sessions/{session}/shares` | Shares for a specific session | 0 |

### POST /v1/chat-shares

Body:
- `session_id` (required, integer): id of an `AiChatSession` owned by the authenticated user
- `title` (optional, string)
- `expires_at` (optional, ISO datetime, must be future)
- `password` (optional, string min 4)
- `max_views` (optional, integer min 1)
- Content toggles (optional, boolean):
  - `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`)
  - At least ONE toggle must be `true`, else `422`.

Response 201:
```json
{
  "data": {
    "id": 1, "token": "<40 chars>",
    "url": "https://rankion.ai/share/<token>",
    "title": "...", "session_id": 42,
    "expires_at": "...", "frozen_at": null,
    "has_password": false, "max_views": null,
    "view_count": 0, "unique_viewer_count": 0,
    "last_viewed_at": null, "revoked_at": null,
    "status": "active",
    "show_user_messages": true,
    "created_at": "..."
  }
}
```

### PATCH /v1/chat-shares/{id}

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

### DELETE /v1/chat-shares/{id}

Soft revoke. Sets `revoked_at`. Public access returns 410. Row stays in DB until daily prune job (90 days post-revoke by default, configurable via `CHAT_SHARES_RETENTION_DAYS`).

### Public route (NOT exposed via API)

`GET /share/{token}` — public, no auth, returns HTML. `noindex` enforced via meta + `X-Robots-Tag` header + robots.txt disallow.
`POST /share/{token}/unlock` — password-form submission; throttle 5/min. Sets path-scoped httpOnly cookie on success.
`GET /share/{token}/print` — clean print-CSS variant of the share view.

---

## Agentic Plans

Read-only API for the agentic-superpowers Brainstorm → Plan → Execute pipeline. Plans are created/mutated EXCLUSIVELY by the master agent's pipeline tools (`build_plan`, `complete_step`, `fail_step`, `skip_step`, `complete_plan`, `clarify_intent`); the HTTP API exposes only inspection. All endpoints require `auth:sanctum` and team-scoping (Policy: `view` only — no `update`/`delete` exposed).

| Method | URL | Description | Credits |
|---|---|---|---|
| GET | `/v1/agentic/sessions/{session}/plans` | Plan history for a session (paginated 50/page, ordered desc by created_at) | 0 |
| GET | `/v1/agentic/plans/{plan}` | Plan detail (status, denormalized counters, brainstorm_summary) | 0 |
| GET | `/v1/agentic/plans/{plan}/steps` | All steps with output_summary, output_data (≤64kB JSON), tool_calls_made, credits_used, latency_ms, retry_count, last_error, subagent_session_id | 0 |
| POST | `/v1/agentic/feedback` | Persists 👍/👎 feedback on an assistant message. Body: `{message_id: int, direction: 'up'\|'down'}`. Idempotent upsert keyed by (user_id, message_id) — second click overwrites direction. Returns 404 unknown_msg, 403 not_yours, 422 invalid direction. Response: `{ok: true, direction}` | 0 |

### Plan response shape

```json
{
  "data": {
    "id": 12, "session_id": 42, "goal": "Konkurrenz-Analyse example.com",
    "brainstorm_summary": null, "strategy": "linear",
    "status": "executing",
    "current_step_id": 35,
    "total_steps": 4, "completed_steps": 1, "failed_steps": 0,
    "started_at": "2026-05-01T08:30:00Z", "completed_at": null,
    "total_tool_calls": 9, "total_credits_used": 5,
    "created_at": "2026-05-01T08:29:55Z"
  }
}
```

### Step response shape

```json
{
  "data": [
    {
      "id": 35, "step_index": 0,
      "label": "Top-10 SERP-Konkurrenten ermitteln",
      "description": "...",
      "complexity": "trivial", "assigned_agent": null,
      "status": "completed", "retry_count": 0, "last_error": null,
      "output_summary": "10 Konkurrenten: ahrefs.com, semrush.com, ...",
      "output_data": { "competitors": ["ahrefs.com", "..."] },
      "tool_calls_made": 1, "credits_used": 1, "latency_ms": 3120,
      "subagent_session_id": null,
      "started_at": "...", "completed_at": "..."
    }
  ]
}
```

`status` values: `pending` | `in_progress` | `completed` | `failed` | `skipped` (steps); `planning` | `executing` | `completed` | `failed` | `aborted` (plans).
`assigned_agent` values: `null` (master itself) | `master` | `analyze` | `explore` | `research` | `action` (Subagent-Persona).

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

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

### `GET /api/v1/openapi.yaml`

OpenAPI 3.1.0 Spec (public, kein Auth nötig). 5-min CDN-Cache. Generiert TypeScript/Python/PHP-SDKs aus z.B. [openapi-generator-cli](https://github.com/OpenAPITools/openapi-generator-cli):

```bash
npx @openapitools/openapi-generator-cli generate \
  -i https://rankion.ai/api/v1/openapi.yaml \
  -g typescript-fetch \
  -o ./rankion-sdk-ts
```

**Schema-Coverage:**
- Subsystem A — `/grounding/analyze`, `/audits/{id}`, `/audits`, `/batch`, `/batches/{id}`, `/batches/{id}/results.ndjson`, `/sitemap-audit`
- Subsystem B — `/content-optimizer/{id}` (canonical + alias)
- Subsystem C — `/agentic/chat`, `/agentic/sessions/{session}`
- Reusable schemas: `GroundingAudit`, `GroundingBatch`, `Finding`, `Tier`, `StructuredError`, `AgenticChatRequest/Response`, `AgenticSessionStatus`

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

### Error-Code-Vokabular (kanonisch)

Alle `code` Werte aus dem `error: {code, ...}` Envelope (Subsystem A + C):

| HTTP | code | Endpoint | Beschreibung |
|---|---|---|---|
| 422 | `context_overflow` | /agentic/chat | 1M-Token-Limit überschritten. Body: `limit_tokens, used_tokens, suggested_action, max_urls_per_call` |
| 502 | `anthropic_api_error` | /agentic/chat | Anthropic-Upstream 5xx. Body: `http_status, message` |
| 404 | `audit_not_found` | /grounding/audits/{id} | ID unbekannt ODER fremdes Team (404 statt 403 gegen Enumeration) |
| 410 | `audit_expired` | /grounding/audits/{id} | TTL gepruned (30d soft + 60d hard) |
| 404 | `batch_not_found` | /grounding/batches/{id} | ID unbekannt ODER fremdes Team |
| 422 | `invalid_url` | /grounding/* | Kein gültiger http(s)-URL |
| 422 | `invalid_framework` | /grounding/* | Framework außerhalb `[v1.5, eeat, people-first]` |
| 422 | `too_many_urls` | /grounding/batch | Batch > 500 URLs |
| 402 | `insufficient_credits` | /grounding/*, /content-optimizer/* | Team-Balance < required |
| 503 | `sitemap_unreachable` | /grounding/sitemap-audit | Sitemap-Parse fehlgeschlagen |
| 404 | `session_not_found` | /agentic/sessions/{id} | Triple-scoped IDOR-Schutz greift |
| 429 | `rate_limited` | (alle Endpoints mit `throttle:`) | Standard Laravel-Throttle. Header `Retry-After` setzt Wartezeit |

### Rate-Limit-Headers

Jeder Endpoint mit `throttle:N,M` Middleware liefert auf erfolgreiche Responses:
- `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).

---

## 🤖 Master-Agent Härtung (Subsystem C)

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

### P1.4 — Strukturierter `context_overflow` Error

Wenn der Agent das 1M-Token-Kontext-Limit hittet, returnt `/agentic/chat` jetzt **HTTP 422** mit strukturiertem Error-Envelope statt 200+German-Text:

```json
{
  "session_id": 1201,
  "error": {
    "code": "context_overflow",
    "limit_tokens": 1000000,
    "used_tokens": 1042000,
    "suggested_action": "split_request",
    "max_urls_per_call": 1
  },
  "text": "⚠️ Diese Anfrage ist zu komplex geworden …"
}
```

Programmatischer Retry ohne String-Matching auf Deutsch. Bei Anthropic-API-Errors (5xx upstream) → HTTP **502** mit `error.code = "anthropic_api_error"`.

### P1.5 — `tool_policy` im Request

Restriktiere welche Tools der Master-Agent in einem Turn nutzen darf — ideal für Audit-Loops, die nur 1-Tool-Calls wollen ohne Token-Blow:

```json
{
  "message": "Audit https://example.com",
  "tool_policy": {
    "allow": ["analyze_grounding_page"],
    "deny": ["web_scrape", "extract_from_urls"],
    "max_tool_calls": 1
  }
}
```

Server-side Enforcement: gesperrte Tools liefern Tool-Result `{success:false, error:"tool X blocked by tool_policy.deny", policy_blocked:true}` — der Agent ignoriert sie und liefert ein Teil-Resultat statt das Limit zu sprengen.

### P1.6 — Async-Default + Polling-Endpoint

`/agentic/chat` blockiert standardmäßig **NICHT mehr** bis zum finalen Resume. Wenn der Agent mid-turn `wait_and_recheck` ausgelöst hat, returnt der Endpoint **HTTP 202** sofort:

```json
{
  "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}`:

```json
{
  "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` | `failed` | `completed`. `is_terminal=true` heißt: keine weitere Hintergrund-Arbeit pending.

**Opt-in legacy blocking:** `POST /agentic/chat?wait_for_final=true` → server polled bis 480s und liefert das finale Result inline (alte Verhalten).

---

## 🧭 Grounding Audit Pipeline (Subsystem A)

Async-Pipeline für LLM-Citation-Readiness-Audits über öffentliche URLs. Single + Bulk + Sitemap mit NDJSON-Stream und HMAC-signierten Webhook-Callbacks.

**Frameworks:**
- **Grounding Page Standard v1.5** (Hanns Kronenberg, https://groundingpage.com/spec/de/) — 22 Page- + 4 Site-Root-Checks
- **Google E-E-A-T** (Search Quality Rater Guidelines + Helpful Content) — *aktuell nur Master-Agent-Pfad, REST-Pipeline-Stub*
- **Google People-First + AI-Features** (developers.google.com) — *aktuell nur Master-Agent-Pfad, REST-Pipeline-Stub*

| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| POST | `/v1/grounding/analyze` | Single-URL-Audit dispatchen. Body: `{url, frameworks?, tracking_project_id?}`. Throttle: 30/min. Returns 202 + `{audit_id, status, estimated_seconds, poll_url}` + `Location:` Header. | 1 |
| GET | `/v1/grounding/audits/{id}` | Polling/Result. Returns `{audit_id, status, score, tier:{name,threshold_min,threshold_max}, frameworks{}, findings[], raw_text, duration_ms, credits_used, started_at, completed_at, expires_at}`. 404 (audit_not_found), 410 (audit_expired). | 0 |
| GET | `/v1/grounding/audits` | Liste eigener Audits. Filter: `?status=, ?tracking_project_id=, ?from=, ?to=`, Pagination. | 0 |
| POST | `/v1/grounding/batch` | Bulk-Audit für 1–500 URLs. Body: `{urls[], frameworks?, concurrency? (1-10), callback_url?, tracking_project_id?}`. Throttle: 5/min. Returns 202 + `{batch_id, url_count, callback_secret, eta_seconds, poll_url, results_url}`. `callback_secret` einmalig. | url_count |
| GET | `/v1/grounding/batches/{id}` | Batch-Status + Counters. Returns `{batch_id, status, source, completed, failed, total, concurrency, eta_seconds, results_url, callback?, started_at, completed_at}`. | 0 |
| GET | `/v1/grounding/batches/{id}/results.ndjson` | NDJSON-Stream completed/failed Audit-Rows. Streamt während Lauf. | 0 |
| POST | `/v1/grounding/sitemap-audit` | Sitemap-Audit. Body: `{sitemap_url, frameworks?, filter:{path_contains?,limit? (max 500),priority_min?}, concurrency?, callback_url?, tracking_project_id?}`. Credits werden NACH Sitemap-Parse abgezogen. | url_count_after_parse |

### Tier-Schwellen

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

### Findings-Schema

Jedes Audit liefert `findings[]` mit Objekten: `{id, severity (critical|high|medium|low|info), framework (v1.5|eeat|people-first), title, description, fix:{type, action, details}, spec_url}`. Zusätzlich `raw_text` (Markdown) für UI-Render.

### Webhook-Verifikation

Server POSTet zu `callback_url` mit Header `X-Rankion-Signature: sha256=<hex>` (HMAC-SHA256 über Body, Key = `callback_secret`) + `X-Rankion-Webhook-Id: <uuid>`. 4 Retries (60/300/1800/7200s) bei 5xx oder 429. Verifikation:
```php
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $callbackSecret);
hash_equals($expected, $request->header('X-Rankion-Signature'));
```

### Error-Codes (Subsystem A)

| HTTP | code |
|---|---|
| 422 | invalid_url, invalid_framework, too_many_urls |
| 402 | insufficient_credits |
| 404 | audit_not_found, batch_not_found |
| 410 | audit_expired |
| 503 | sitemap_unreachable |
| 429 | rate_limited |

**Beispiel — Single-URL Audit:**
```bash
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:**
```bash
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`.

## 📚 Wiki

Public + internal wiki content. Markdown-Files in `docs/wiki/` werden via `php artisan wiki:sync` (auto bei jedem Deploy) in die DB gespiegelt. Slug-Hierarchie via `/`: `modules/backlinks`. Wiki-interne Links via `[[slug]]` oder `[[slug|Anzeigetext]]`.

| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | `/v1/wiki/pages` | Liste aller Pages (paginiert 50/Seite). Filter: `?category=modules`, `?visibility=public\|internal`. Sanctum-User sehen auch `internal`-Pages. | 0 |
| GET | `/v1/wiki/pages/{slug}` | Single Page mit `body_md`, `body_html`, `toc[]` (level/text/anchor), `related[]`. Slug akzeptiert Slashes für Hierarchie. | 0 |
| GET | `/v1/wiki/search?q=...` | Top-20 FULLTEXT-Treffer (title+description+body_md). Title-Boost 2×. Snippets mit `<mark>`-highlighted Tokens. Min. 2 Zeichen. | 0 |

Public-Reader: `https://rankion.ai/wiki/{slug}` — server-rendered HTML, Sidebar+TOC, `/`-Shortcut für Search-Focus. `visibility: internal` → 401 für anonyme User.

Master-Agent-Integration: `query_wiki(query)` Master-Tool returnt Top-3 mit vollen URLs — Trigger bei "wie geht X", "wie aktiviere ich Y", "was ist Z?".

## 🧠 Deep Insights (LLM Data Mining)

Tiefenanalyse über alle 6 Mining-Schichten (Atomisierung → Quellenscan → Cross-Pattern → Faktencheck → Forecast → Action). Liefert pro Projekt einen 9-Sektionen-Report (Executive Summary, Faktenfehler, Konsens-Narrative, Persona-Lücken, Outreach Top-50, Wettbewerbs-Co-Occurrence, Frühwarn-Radar, Source-Whitelist, 6-Wochen-Forecast). Voraussetzung: Atomisierung läuft (Layer 1 backfill); für Faktencheck zusätzlich Truth-Source mit `human_reviewed=true`.

| Method | URL | Beschreibung | Credits |
|---|---|---|---|
| GET | `/v1/data-mining/insights/{project}` | Voller 12-Sektionen-JSON-Payload (alle Sections in einer Response). | 0 |
| POST | `/v1/data-mining/insights/{project}/refresh` | Triggert Insights-Report-Generierung (PDF + CSV-ZIP-Bundle, asynchron). HTTP 202. | 0 |
| GET | `/v1/data-mining/insights/{project}/section/{section}` | Einzel-Section-Lazy-Load. `{section}` ∈ `executive_summary, fact_errors, consensus_narratives, consensus_clusters, persona_gaps, battle_cards, outreach_top50, competitor_cooccurrence, early_warnings, source_whitelist, forecast_6w, recommended_actions`. | 0 |
| GET | `/v1/data-mining/exports/{project}.pdf` | Letzten generierten PDF-Export downloaden. 404 falls noch keiner existiert (vorher `refresh` aufrufen). | 0 |
| GET | `/v1/data-mining/exports/{project}.zip` | Letztes CSV-Bundle (1 CSV pro Section, gezippt). 404 wenn nicht vorhanden. | 0 |

Alle Endpoints Sanctum-authed mit Team-Scope-Check (`tracking_projects.team_id === user.currentTeam->id`, sonst 403).

**Wöchentlicher Insights-Report:** Jeden Sonntag 18:00 wird für alle Projekte der PDF-Report generiert und mit Highlights-Summary an den Team-Owner gemailt.

**Live-Dashboard:** `https://rankion.ai/ai-visibility/{id}/insights` — interaktive 9-Tab-Ansicht des gesamten Payloads.

