Zum Hauptinhalt springen

AgencyOS: cockpitOS anbinden (Magic Link & Integration API)

Diese Seite richtet sich an Entwickler:innen von AgencyOS (Team-Produkt unter team.cockpit-os.de). Sie beschreibt, was AgencyOS implementieren muss, um eine Vertrauensstellung mit dem cockpitOS-Dashboard herzustellen und Center sowie Inhalte über die REST-API anzusprechen. Dabei gibt es zwei Geltungsbereiche für den API-Key: eine Organisation (klassisch) oder nutzerweit (alle Center gemäß den Cockpit-Rechten der anbindenden Person, organisationenübergreifend inkl. zugewiesener Center ohne Organisation).

Produkt: AgencyOS · Dashboard-Implementierung (Monorepo): u. a. apps/dashboard/src/app/api/agencyos/…, apps/dashboard/src/app/agencyos/connect/page.tsx

Schlüssel ohne Magic Link (im Dashboard): Eingeloggte Nutzer:innen mit passender Berechtigung können unter Einstellungen → Integrationen eine AgencyOS-Integration anlegen bzw. den API-Key rotieren (GET/POST /api/agencyos/integrations, POST/rotate) — sinnvoll z. B. für lokale Tools (MCP, Skripte). Vollständiger Key wie gewohnt einmalig in der Antwort, nicht dauerhaft in der UI.

MCP-Tool-Referenz: Alle registrierten Tools mit Parametern und Beispiel-Prompts — MCP-Tool-Referenz.

Remote-MCP (HTTP, Monorepo): Paket packages/mcp-cockpit-remote stellt dieselben MCP-Tools per Streamable HTTP bereit (für Organisationen mit Claude-„Remote MCP“-URL). Nicht Teil der öffentlichen Dashboard-API; Betrieb mit eigenem Secret COCKPIT_MCP_HTTP_BEARER und COCKPIT_AGENCYOS_API_KEY im Server-Env. Siehe Paket-README.

Basis-URL

Ersetzen Sie {DASHBOARD_ORIGIN} durch die öffentliche URL Ihrer Dashboard-Instanz (z. B. https://dashboard.cockpit-os.de). Lokal oft http://localhost:3000. Die Variable NEXTAUTH_URL im Dashboard bestimmt die in Magic-Link-Antworten eingebettete Basis-URL.

Überblick: Was AgencyOS tun muss

  1. Magic Link anfordern (serverseitig, ohne Nutzer-Session im Cockpit): POST /api/agencyos/magic-link mit integrationName und optional returnUrl.
  2. Nutzer:in zum Cockpit leiten: Response enthält data.magicLink (Pfad /agencyos/connect?token=…). Dort meldet sich eine berechtigte Person an und wählt entweder eine Organisation oder „Alle Center mit meinen Zugriffsrechten“ (nutzerweiter Key).
  3. API-Key per Polling holen: Solange status === "pending", regelmäßig GET /api/agencyos/magic-link?token=… aufrufen. Sobald status === "completed", liefert die Antwort data.apiKey (Präfix typisch sk_agencyos_) sowie data.accessScope ("organization" oder "user") und data.organizationId (UUID oder null bei nutzerweitem Key).
  4. Speichern: API-Key sicher in AgencyOS hinterlegen (Geheimnis, nicht in Logs/URLs).
  5. API nutzen: Alle folgenden Aufrufe mit Authorization: Bearer <apiKey> gegen /api/agencyos/v1/….

Sicherheit und Redirect

  • Den API-Key niemals in die Redirect-URL legen (kein Query-Parameter mit Secret). Die Übernahme erfolgt ausschließlich über das Polling des Magic-Link-Status.
  • Wenn ihr eine returnUrl (z. B. zurück nach https://team.cockpit-os.de/...) mitsendet, kann das Cockpit nach erfolgreichem Abschluss dorthin weiterleiten und setzt u. a. agencyos=connected sowie cockpit_agency=connectedohne Key. Den Key nur aus der GET-Antwort lesen, wenn status === "completed".

Unterschied zu WordPress

AspektWordPress-PluginAgencyOS
Geltungsbereichein Center pro Website-KeyOrganisation: alle Center dieser Org · Nutzer: alle Center, auf die die anbindende Person in Cockpit Zugriff hat (mehrere Orgs + ohne Org)
Verbindungs-UI/wordpress/connect/agencyos/connect
Content-PushPOST /api/wordpress/push-content (Key = Website)POST /api/agencyos/v1/content/push mit centerId im JSON
Doku Push-BodyWordPress Push-ContentGleiche Entity-Arrays (shops, events, …); siehe unten

POST {DASHBOARD_ORIGIN}/api/agencyos/magic-link

  • Auth: keine (öffentlich wie beim WordPress-Magic-Link).
  • CORS: Access-Control-Allow-Origin: *, OPTIONS unterstützt.

Body (JSON):

FeldTypPflichtBeschreibung
integrationNamestringjaAnzeigename der Integration in Cockpit (z. B. "AgencyOS Produktion")
returnUrlstringneinhttp:// oder https://; nach Erfolg optional Redirect aus dem Browser

Erfolg (200):

{
"success": true,
"data": {
"magicLink": "https://…/agencyos/connect?token=mla_…",
"token": "mla_…",
"expiresAt": "2026-04-01T12:00:00.000Z"
},
"message": "Magic Link erstellt"
}

Hinweis: Token-Gültigkeit 15 Minuten ab Erstellung (sofern nicht vorher abgeschlossen).


GET {DASHBOARD_ORIGIN}/api/agencyos/magic-link?token=<token>

  • Auth: keine.

Solange die Verbindung aussteht, ist data.status typischerweise "pending". Nach Abschluss im Browser:

  • data.status === "completed"
  • data.apiKey gesetzt
  • data.integrationId gesetzt
  • data.accessScope: "organization" oder "user"
  • data.organizationId: UUID der verbundenen Organisation oder null, wenn accessScope === "user"

AgencyOS-Implementierung: Nicht von einem festen organizationId im Key ausgehen. Bei accessScope === "user" ist organizationId absichtlich null; die erlaubten Center ergeben sich aus den Cockpit-Rechten des Nutzers (siehe GET /v1/centers).

Fehler: u. a. 404 ungültiger Token, 410 abgelaufen (bei noch pending).


3. Verbindung im Browser abschließen (nicht von AgencyOS-Server)

Dieser Schritt läuft im Cockpit mit NextAuth-Session; AgencyOS ruft ihn normalerweise nicht per Server-to-Server auf.

  • UI: GET /agencyos/connect?token=…
  • Organisationen laden (Session): GET /api/agencyos/organizations (für die klassische Variante; nutzerweite Option ist auch ohne Einträge in der Liste möglich)
  • Abschluss: POST /api/agencyos/magic-link/complete mit JSON:
    • Organisations-Key: { "token": "…", "organizationId": "<uuid>" }
    • Nutzer-Key: { "token": "…", "accessScope": "user" } (kein organizationId nötig)

Die Response enthält u. a. apiKey, accessScope, organizationId (nullable) für die sofortige Anzeige im Browser – für AgencyOS ist weiterhin das Polling die Quelle der Wahrheit, damit der Backend-Prozess den Key zuverlässig erhält.


4. AgencyOS API v1 (Bearer API-Key)

Alle Endpunkte unter /api/agencyos/v1/ erwarten:

Authorization: Bearer <apiKey>

apiKey ist der aus Schritt 2 übernommene AgencyOS-Integration-Key (sk_agencyos_…) – entweder an eine Organisation oder nutzerweit gebunden (accessScope aus der Polling-Antwort).
CORS: Access-Control-Allow-Origin: * (u. a. für GET, POST, PATCH, OPTIONS).

Medien-Upload zu Bunny (Bilder & Videos)

POST {DASHBOARD_ORIGIN}/api/agencyos/v1/media/upload

  • Auth: Authorization: Bearer <apiKey>
  • JSON (Variante A): { "url": "https://…", "folder?": "agencyos/uploads" } — Ressource wird geladen und nach BunnyCDN kopiert (Bild oder Video).
  • JSON (Variante B): { "base64": "…", "mimeType?": "video/mp4", "filename?": "…", "folder?": "…" } — optional mit data:mime;base64,-Präfix.
  • Raw Body: Content-Type: image/* oder video/* oder application/octet-stream; Query ?folder=&filename= — Rohbytes direkt nach Bunny.
  • Größenlimits: in etwa 10 MB für typische Bilder, 100 MB für Video (Implementierung in route.ts).
  • Antwort: { "success": true, "bunnyUrl": "https://…b-cdn.net/…" } (bereits Bunny-URLs werden unverändert bestätigt).

MCP: Paket @mall-os/mcp-cockpit-os, Tool cockpit_upload_media — Parameter url oder base64 (plus optional mimeType, filename, folder).

Mediathek listen: GET {DASHBOARD_ORIGIN}/api/agencyos/v1/media?centerId=<uuid> — Filter type, entityType, q, includeShared, limit, offset. MCP: cockpit_list_media.

UI (Redaktion): Video wie Bilder über FileUploadPOST /api/upload (Multipart, Bunny-Pfad z. B. centers/{centerId}/{entityType}/video/…) und „Aus Mediathek“; nicht zu verwechseln mit dieser AgencyOS-JSON-Route.

4.1 Shopping Center auflisten

GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers

Response (200): { "success": true, "data": [ { "id", "name", "slug", "address", "city", "postalCode", "country", "status", "websiteEnabled", "organizationId", "agencyIntegrationId", "updatedAt" }, … ] }
Max. 500 Einträge, sortiert nach Name.

Bei accessScope === "organization" (Standard):

  • alle Center mit organizationId = Organisation des API-Keys, und
  • Center ohne Organisation (organizationId: null), die von genau dieser Agency-Integration angelegt wurden (agencyIntegrationId = Integration des Keys).

Bei accessScope === "user":

  • alle Shopping Center, auf die der anbindende Cockpit-Nutzer Zugriff hat (z. B. über UserCenterAssignment, Heimat-Organisation, Super-Rollen – wie in Cockpit definiert), und
  • dieselben integrationsgebundenen „Waisen“-Center wie oben (ohne Organisation, aber agencyIntegrationId dieser Integration).

So erscheinen „freie“ Center nur im Kontext der eigenen Integration, ohne fremde ungebundene Center zu leaken.

4.2 Shopping Center anlegen

POST {DASHBOARD_ORIGIN}/api/agencyos/v1/centers

Body (JSON) – Pflichtfelder:

FeldTypBeschreibung
namestring
addressstring
citystring
postalCodestring

Optional: country (Default "DE"), slug (sonst automatisch aus Name, global eindeutig), description, phone, email, website, status (Default "active").

Organisation (wie im Cockpit):

FeldTypBeschreibung
Standard (Organisations-Key)Ohne die folgenden Felder wird organizationId auf die Organisation des API-Keys gesetzt.
Standard (Nutzer-Key)Nicht ohne Ziel-Org: Es muss entweder withoutOrganization/noOrganization/organizationId: null oder eine explizite organizationId (UUID) gesendet werden, für die der anbindende Nutzer in Cockpit berechtigt ist; sonst 400/403.
withoutOrganization / noOrganizationboolean trueCenter wird ohne Organisation angelegt (organizationId: null), bleibt aber dieser Integration zugeordnet (intern agencyIntegrationId), damit sie es in GET/Push weiter nutzen kann.
organizationIdnullGleiche Bedeutung wie withoutOrganization: true.
organizationIdstring (UUID)Organisations-Key: nur erlaubt, wenn der Wert exakt der Organisation des Keys entspricht; sonst 403. Nutzer-Key: erlaubt, wenn der Nutzer diese Organisation verknüpfen darf (wie im Cockpit); sonst 403.

Erfolg: HTTP 201, data enthält u. a. organizationId und agencyIntegrationId.

Fehler: 409 wenn slug bereits vergeben.

4.3 Einzelnes Center lesen / aktualisieren

GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}
PATCH {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}

Zugriff, wenn das Center für diesen Key erlaubt ist (Organisations-Key: gleiche Organisation oder integrationsgebundenes Waisen-Center; Nutzer-Key: gemäß Nutzerrechten oder integrationsgebundenes Waisen-Center); sonst 404.

PATCH: nur gesendete Felder werden geändert. Erlaubt u. a. name, address, city, postalCode, country, phone, email, website, description, openingHours, status, slug, websiteEnabled. Pflichtfelder dürfen nicht auf null gesetzt werden.

Später an eine Organisation hängen: assignToKeyedOrganization, assignToOrganization oder linkToKeyedOrganization mit true – nur wenn das Center aktuell ohne Organisation ist und von dieser Integration stammt.

  • Organisations-Key: Es wird die Organisation des Keys verbunden (agencyIntegrationId wird entfernt).
  • Nutzer-Key: Zusätzlich attachOrganizationId (UUID) im JSON-Pflicht – Zielorganisation, an die gehängt werden soll; nur wenn der anbindende Nutzer dafür in Cockpit berechtigt ist; sonst 403. (agencyIntegrationId wird entfernt.)

Website-Konfiguration (GET + partial PUT)

Lesen: GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/website-config
Liefert designConfig, seoConfig, contentConfig, legalConfig, parkingConfig, analyticsConfig, centerplanConfig, pagesConfig, templateContent (Template-Reiter).

Schreiben: PUT {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/website-config
Partial-Update; templateContent wird deep-merged.

Reiter-Index (Schema): GET {DASHBOARD_ORIGIN}/api/agencyos/v1/website-config-schema?websiteTemplate=ilg
oder ?centerId={uuid} — Dashboard-Reiter mit Speicherort, fields[] (exakte JSON-Pfade), v0PublicRead (öffentliche Live-Site) und MCP-Hinweisen für alle Website-Templates.

v0 / Claude Live-Website (LESEN, ohne Auth):
GET …/api/centers/{centerId}/public-visitor-surfacedata.templatePublicContent (Template-Reiter: Hero, Footer, …) + data.apiHints.pageContentGet / homepageTilesGet.
Nicht GET …/website-config im Browser — 401. Schreiben bleibt MCP: cockpit_*. Antwort enthält v0Integration (Lesen-vs-Schreiben-Guide).

Page Content (Hero, SEO, customContent): POST …/page-content — beim Update werden nur mitgesendete Scalar-Felder geändert; customContent wird deep-gemerged (z. B. customContent.ilg.anfahrtBoxes ohne andere Seitenfelder zu löschen).

Empfohlener MCP-Workflow (ILG/RGW):

  1. cockpit_website_config_schema (mit websiteTemplate oder centerId)
  2. Reiter aus tabs[] wählen → fields[] / templateContentPath / customContentPath lesen
  3. Bestehende Werte per GET (get_center_website_config / page_content)
  4. Partial PUT/upsert nur mit geänderten Keys
  5. Revalidate (automatisch in API-Response, sofern konfiguriert)

MCP:

ToolFunktion
cockpit_get_center_website_configVolle Config lesen
cockpit_update_center_website_configPartial Update
cockpit_website_config_schemaReiter + Feldpfade pro Template
cockpit_mcp_discover_toolsTool-Index wenn Claude tool_search scheitert

contentConfig.specialDays (Sonderöffnungszeiten): JSON-Array oder dasselbe als JSON-String. Pro Eintrag z. B. { "date": "2026-12-24", "label": "Heiligabend", "hours": { "open": "10:00", "close": "14:00" } } — geschlossen mit "hours": null. Optional "image" (URL).

Die Route schreibt in ShoppingCenter.specialDays und spiegelt bei strukturiertem openingHours (mit regularHours) zusätzlich openingHours.specialDays — analog zum Speichern im Cockpit „Center bearbeiten“.

4.3a Center-Kontext für KI lesen (Shops / Services)

GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/context

Zugriff wie GET /v1/centers/{centerId} (Bearer-Key, sonst 404).

Liefert gebündelte Lesedaten für AgencyOS/KI, ohne die öffentliche Website-API zu nutzen:

QueryDefaultMaxBeschreibung
includecenter,shopsKomma-getrennt: center, shops, services, news, events, offers, categories, chains, floors_summary
shopLimit5002000Max. Anzahl kombinierter Einträge (Einzelshops + Filialen), sortiert nach Name
serviceLimit200500Nur wenn services in include
newsLimit80200Nur wenn news in include
eventsLimit80200Nur wenn events in include
offersLimit80200Nur wenn offers in include

Filter: Shops und Filialen wie GET …/centers/{centerId}/shops?publicWebsite=true&status=Aktiv (inkl. Veröffentlichungsfenster). Services wie websiteServicePublicFilter (Center-Website-SSR).

News / Events / Angebote: Alle Datensätze dieses Centers (jeder Status, inkl. Entwurf), sortiert nach updatedAt absteigend — für Kontext, Dedupe und Abgleich mit AgencyOS. Keine Volltexte; pro Zeile u. a. id, title, slug, status, Daten, source, sowie agencyosPushId und wordpressPushId aus metadata, falls gesetzt.

categories: Wie getWebsiteShopCategories (Center + Global, minShopCount: 0, Status Aktiv für Zählung) — nur Referenzen zur Token-Optimierung: je Eintrag id, name, slug (keine Zähler/Icons/Farben im Kontext).

chains: Alle ShopChain, die über Shops oder Filialen (ShopLocation) an dieses Center angebunden sind (max. 500, nach Name) — nur id, name, slug.

floors_summary: Für jede aktive Etage ein kompaktes Objekt — u. a. floorId, name, floorNumber, mapSvgChars (Zeichenzahl von mapSvg ohne Auslieferung des Strings), suggestsHybridSvg (Heuristik: typische Hybrid-Marker im Markup), hasMapImageUrl, hasShopViewBoxes, shopViewBoxesKeyCount, mapLocationActiveCount. Für Svg-Markup, mapLocations und Routenplanung weiter die öffentliche Route GET /api/wayfinding/floors?centerId= oder das MCP-Tool cockpit_public_wayfinding_floors.

Response (200): { success: true, data: { center?, shops?, services?, news?, events?, offers?, categories?, chains?, floors_summary? }, meta: { …limits, counts } }
data.shops[]: kompakte Objekte u. a. id, name, category, slug, floor, location, status, isShopLocation, chain, logo, coverImage, displayId.

4.3b Wartung: Duplikate, Ketten-Merge, Domains, Centerplan

Zusätzliche AgencyOS v1-Routen (Bearer, Center-Zugriff wie bei anderen /v1/centers/{centerId}-Endpunkten):

RouteMethodeZweck
…/centers/{centerId}/shop-duplicatesGETPotenzielle Shop-Duplikate im Center (Namens-Ähnlichkeit, Query threshold 0.5–1, optional includeArchived). Antwort: Gruppen mit suggestedKeepId und archiveCandidateIds.
…/chains/duplicatesGETPotenzielle ShopChain-Duplikate global (Query threshold, Default 0.8).
…/chains/mergePOSTKetten zusammenführen (Quelle → Ziel): Filialen/Einzelshops umhängen, Quell-Kette löschen. Body: sourceChainId + targetChainId, optional dryRun, oder Bulk pairs[] (max. 25).
…/centers/{centerId}/verify-domainsPOSTDNS/HTTPS für Custom Domains prüfen und domainStatus / sslStatus in der DB aktualisieren.
…/centers/{centerId}/wayfinding/floorsGETKompakte Etagen-Liste (IDs, Metriken, Zähler) ohne mapSvg-Body — für MCP/KI; volles SVG weiter GET /api/wayfinding/floors.
…/centers/{centerId}/map-locationsGET, POSTMapLocations listen (floorId, activeOnly) bzw. anlegen.
…/centers/{centerId}/map-locations/assignPOSTShop/Service an SVG-Fläche zuordnen (floorId, svgId, upsert).
…/map-locations/{locationId}PATCHMapLocation aktualisieren (Zuordnung, Geo, aktiv/inaktiv).

Hilfslogik: apps/dashboard/src/lib/integration/ (find-shop-duplicates, find-chain-duplicates, merge-shop-chains, shop-name-similarity, agency-map-location). Duplikat-Archivierung kann über POST …/content/bulk-archive mit contentType: "shop" erfolgen.

4.4 Inhalte ins Center pushen

POST {DASHBOARD_ORIGIN}/api/agencyos/v1/content/push

Gleiches Konzept wie WordPress Push-Content: Body mit optionalen Arrays shops, events, news, offers, services.
Zusätzlich Pflicht:

FeldTypBeschreibung
centerIdstringUUID des Centers; zulässig unter denselben Regeln wie GET /v1/centers/{centerId} (Organisations-Key vs. Nutzer-Key inkl. Waisen dieser Integration)

Hinweise:

  • Es gibt keine WordPress-pages-Speicherung in WordPressWebsite; Fokus liegt auf den Entitäts-Arrays.
  • In Cockpit werden betroffene Inhalte mit source agencyos markiert (analog wordpress beim WP-Endpunkt).

Idempotenz (Events, News, Angebote, Services — wie Shops)

Für events[], news[], offers[], services[] gilt dieselbe externe Referenz wie bei Shops:

  • agencyosId, externalId, clientReference oder redaktionsReferenz, alternativ bei AgencyOS-Push ein id, das keine Cockpit-UUID und kein wp_* ist.
  • Cockpit speichert den Wert unter metadata.agencyosPushId und findet beim nächsten Push dieselbe Zeile zum Update (kein Duplikat).
  • Reihenfolge der Zuordnung: Cockpit-UUIDagencyosPushIdWordPress-wordpressPushIdslug → (nur WordPress) Legacy nach Titel.

Zusätzlich: publishDate bei News akzeptiert auch Alias publishedAt oder date.

Shops: Ketten & KI-Redaktion

Redakteure arbeiten in AgencyOS oft per natürlicher Sprache mit einer KI. Die API ist so ausgelegt, dass die KI nicht zwingend Cockpit-UUIDs kennen muss:

ZielEmpfohlene Felder im shops[]-Eintrag
Dieselbe Marke nicht hundertmal anlegenPro logischem Shop eine stabile Referenz: agencyosId oder externalId (oder bei source-Push id als freier String, keine UUID) – wird im Cockpit unter metadata.agencyosPushId gespeichert und beim nächsten Push zum Update verwendet. Zusätzlich slug pro Center, falls vorhanden.
„Deichmann ist eine Kette“kette, marke, chainName, brandName oder shopChainName mit dem Markennamen → Cockpit verbindet den Shop mit dieser ShopChain (existiert sie nicht, wird sie angelegt; nur neue Kettendaten, keine Löschungen).
Bereits bekannte Kette (UUID aus Cockpit)shopChainId oder chainId setzen.
Nur Einzelshop ohne KettestandaloneShop / einzelshop / clearShopChain: true oder modus: "einzel" / "standalone".

Fachlicher Hinweis: Im Cockpit gibt es zusätzlich Filialen als ShopLocation (Kette + Center). Der Push landet zunächst auf dem Shop inkl. shopChainId-Verknüpfung; eine vollständige Filialen-Spiegelung ist davon getrennt und kann bei Bedarf später ergänzt werden.

Filialen: Branche pro Standort (nicht Kette)

Wenn dieselbe Kette in mehreren Centern unterschiedliche Branchen-Namen braucht (z. B. Aufräumen doppelter Kategorie-Labels nur in einem Center):

SchrittAPI / MCP
Kategorien lesenGET …/categories?centerId=… oder cockpit_list_categoriesid der Ziel-Kategorie
Filiale findencockpit_list_shop_locations (centerId, ggf. chainSlug) → locationId
Override setzenPUT …/shop-locations/{locationId} mit { "categoryId": "<uuid>" } oder MCP cockpit_update_shop_location
Wieder Kette erben{ "categoryId": null }

Nicht cockpit_update_chain / Ketten-category ändern, wenn andere Center die Ketten-Branche unverändert lassen sollen. Die öffentliche Website wertet ShopLocation.categoryRef vor ShopChain.category aus.

Ausführliche Feldliste (inkl. WordPress): WordPress Push-Content – shops.

Response: analog WordPress: success, data.summary, optional data.errors.

4.4a Push-Vorschau (keine DB-Änderung)

POST {DASHBOARD_ORIGIN}/api/agencyos/v1/content/push/preview

  • Gleicher JSON-Body wie POST …/content/push (inkl. centerId), gleicher Bearer-API-Key und Center-Zugriff.
  • Keine create/update-Schreiboperationen in der Datenbank — geeignet für Freigabe-Workflows (z. B. AgencyOS zeigt die geplanten Änderungen, danach echter Push).
  • Response (200): success, message, data.previewPlan (Liste geplanter Schritte mit entity, action create/update, match bei Zuordnung, existingId, planned, optional notes), data.summary, optional data.errors (Validierungs-/Ketten-Fehler wie beim echten Push).
  • Shop-Ketten: Würde eine neue ShopChain angelegt (namensbasierte Auflösung), erscheint das in planned.shopChain als would_create_chain ohne die Kette anzulegen; Hinweis ggf. in notes.
  • GET …/preview: wird nicht unterstützt (405) — JSON-Payload gehört in den POST-Body.

Content-Entwürfe (Workflow) — Lesen, Freigabe, Kundenkontakt

Dieselbe ContentDraft-Logik wie unter Workflow & Freigaben im Dashboard, per AgencyOS-Key:

MethodePfadKurzbeschreibung
GET/api/agencyos/v1/draftsListe; Query wie bisher (status, contentType, centerId, campaignLabel, limit). Neu: includeData=1 — pro Eintrag geparstes data (Längen serverseitig begrenzt), u. a. customerCommunication, dispatchItemId, Inhaltsfelder.
GET/api/agencyos/v1/drafts/{draftId}Ein Entwurf; optional ?includeData=1.
PUT/api/agencyos/v1/drafts/{draftId}approve / reject (unverändert).
POST/api/agencyos/v1/drafts/{draftId}/customer-touchpoint-suggestionBody: { "scenario": "workflow_approve" | "workflow_reject" | "workflow_pending", "rejectionReason"?: string }gleiche KI-Vorschläge wie im Dashboard (Betreff, E-Mail-Entwurf, interne Hinweise). Nutzt die globale Cockpit-KI-Konfiguration.

MCP/Claude: cockpit_list_drafts (optional includeData: true), cockpit_get_draft, cockpit_draft_customer_touchpoint, cockpit_update_draft. Pro Entwurf: createdBy, createdByName, source, createdAt.


Audit-Log (Provenance)

GET {DASHBOARD_ORIGIN}/api/agencyos/v1/audit-logs

Bearer-API-Key; Center-Zugriff wie bei anderen v1-Routen.

QueryBeschreibung
centerIdLetzte Änderungen im Center (Tagesüberblick) — oder
entityType + entityIdHistorie eines Eintrags (offer, news, shop, event, service, job, center, shop-location)
limitDefault 50, max 100
includeValues1 / trueoldValues / newValues mitliefern (Feldänderungen)

Response (200): success, centerId, count, data[] mit u. a. action (CREATE/UPDATE/DELETE), entityType, entityId, entityLabel, actorDisplay (primary, secondary, isIntegration), userName, userId, timestamp, optional metadata.

MCP: cockpit_audit_logs — z. B. „Wer hat dieses Angebot zuletzt bearbeitet?“ (entityType + entityId aus search_content). Mit summary: true / Query summary=1: Rangliste byUser (ideal für „wer war am aktivsten?“). Ab Deploy: auch MCP-Push, Uploads und Center-Anlage via AgencyOS werden protokolliert (metadata.integrationName, channel: agencyos).


Center-Team (Zugriff & Rollen)

GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/team

Bearer-API-Key; Center-Zugriff wie bei anderen v1-Routen.

Response (200): center, count, data[] mit zugewiesenen Nutzer:innen (user.name, user.email), centerRole, Content-/Shop-Berechtigungen, assignedAt.

MCP: cockpit_center_team — z. B. „Wie viele Personen haben Zugriff auf Center X?“


Website-Cache (Revalidate)

Nach POST …/content/push (Direkt-Push, keine Drafts) invalidiert das Dashboard automatisch den Next.js-Cache auf allen in CENTER_WEBSITE_URLS / CENTER_WEBSITE_URL eingetragenen Instanzen — wenn REVALIDATION_SECRET gesetzt ist. Die Push-Antwort kann data.revalidation enthalten (instances[] pro URL).

Manuell: POST {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/revalidate (Bearer-Key, Body optional { "fullRevalidation": true }).

MCP: cockpit_revalidate_website

v0/Vercel: Live-Seite braucht app/api/revalidate/route.ts, REVALIDATION_SECRET auf Vercel (identisch zum Dashboard) und websitePublicUrl im Center (automatisch via Deploy-Registrierung oder manuell im Dashboard). Legacy: globale CENTER_WEBSITE_URLS. Ohne Revalidate-Secret: Daten in Cockpit aktuell, Vercel kann ISR-Cache zeigen.


Frontend-Kanäle lesen (Cockpit vs. v0)

GET {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/frontend-channels

Bearer-API-Key; gleiche Übersicht wie Dashboard Website-Management → Frontend-Kanäle (v0).

Response (200): success, data mit u. a.:

FeldBedeutung
channels.website.cockpitPreview{slug}.cockpit-os.de
channels.website.v0VercelStaging / v0VercelProductionRegistrierte v0/Vercel-URLs
resolvedUrls.websiteEffektiver Dashboard-Link (v0 wenn gesetzt, sonst Cockpit)
frontendDeploymentMetaLetzte Meldung pro Kanal (registeredAt, origin, …)
interpretation.isV0WebsiteConnectedKurz: v0 schon gemeldet?
websiteStackStatusCockpit / Vercel / Live-Status
apiHintsu. a. qrResolveGet, qrScanPost, qrTrackPost, companionQrPath, websiteQrPath (Standort-QRs, siehe API-Vertrag)

MCP: cockpit_frontend_channels — nach Deploy prüfen; fehlt Meldung → cockpit_register_frontend_deployment. data.apiHints enthält Standort-QR-Vertrag (qrResolveGet, qrScanPost, …).

Standort-QRs (MCP): cockpit_qr_codesaction=list|get|create|resolve; öffentliche Auflösung wie v0 /companion/qr/[qrCodeId]. Nicht Handoff (/api/public/handoff-sessions).

Hinweis: Keine Route-Registry pro Pfad — nur Kanal-URLs (Cockpit-Subdomain vs. Vercel vs. Custom Domain).


Website-Live-Inventar (Bulk)

GET {DASHBOARD_ORIGIN}/api/agencyos/v1/website-live-inventory

Bearer-API-Key; liefert für alle zugänglichen Center (max. 500) den Website-Live-Status in einem Aufruf — statt pro Center frontend-channels zu pollen.

Query-Parameter (optional):

ParameterBeschreibung
chainSlug / chainNameWie bei GET …/centers — nur Center mit Filiale dieser Kette
organizationId / organizationNameNur Center einer Organisation
websiteEnabledOnly=trueNur Center mit aktivierter Website
excludeTestLike=trueTest-/Demo-Center (Name/Slug) ausblenden

Response (200): success, data.items[], data.summary, meta.

Pro Eintrag in items:

FeldBedeutung
bucketoff | cockpit_only | v0_preview | v0_production | custom_domain_live
isV0WebsiteConnectedv0/Vercel-Deploy gemeldet?
urls.cockpitPreview / v0Staging / v0Production / customDomainKanal-URLs
urls.activeSurfaceEffektive Besucher-Oberfläche (Priorität: Production → Domain → Staging → Cockpit)
domainStatus / domainVerifiedDNS/Domain-Stand
websiteStackOverallLeveloff | configured | pending | active | warning

summary.byBucket zählt Center pro Klasse; summary.v0ConnectedCount und customDomainActiveCount für schnelle KPIs.

MCP: cockpit_website_live_inventory — gleiche Filter als Tool-Parameter.

Hinweis: Kein HTTP-Ping auf Live-URLs — Klassifikation aus Cockpit-Daten (wie Dashboard „Frontend-Kanäle“). Für einen einzelnen Center-Detailstand weiterhin cockpit_frontend_channels oder cockpit_dns_status.


Outstand-Kanal-Inventar (Bulk)

GET {DASHBOARD_ORIGIN}/api/agencyos/v1/social/center-accounts

Bearer-API-Key; liefert für alle zugänglichen Center (max. 500) die Outstand.io-Kanal-Verknüpfungen in einem Aufruf — statt pro Center website-config zu lesen (dort sind socialMedia nur Website-Links, kein Outstand).

Query-Parameter (optional):

ParameterBeschreibung
chainSlug / chainNameWie bei GET …/centers — nur Center mit Filiale dieser Kette
organizationId / organizationNameNur Center einer Organisation
centerIdEinzelnes Center (UUID)
connectedOnly=trueNur Center mit mindestens einem aktiven Outstand-Kanal
activeAccountsOnly=trueIn accounts nur aktive Mappings (isActive=true)
excludeTestLike=trueTest-/Demo-Center (Name/Slug) ausblenden

Response (200): success, data.items[], data.summary, meta.

Pro Eintrag in items:

FeldBedeutung
outstandConnectedMindestens ein aktiver Kanal verknüpft?
activeAccountCount / totalAccountCountAnzahl aktiver bzw. aller Mappings
accounts[]outstandAccountId, network, username, isActive, …

summary.centersWithOutstand / centersWithoutOutstand für schnelle KPIs.

MCP: cockpit_list_outstand_centers — gleiche Filter als Tool-Parameter.

Hinweis: Liefert Verknüpfungsstand aus center_social_accounts, keine Outstand-Performance-Metriken (Reichweite, Follower). Dafür weiterhin Dashboard Social Reporting.


Frontend-Deploy registrieren (v0 → Cockpit)

POST {DASHBOARD_ORIGIN}/api/agencyos/v1/centers/{centerId}/frontend-deployments/register

Bearer-API-Key; Center-Zugriff wie bei anderen v1-Routen.

Body (JSON):

FeldTypPflichtBeschreibung
channelstringjawebsite | signage | companion
originstringjaDeploy-Origin ohne Pfad, z. B. https://xyz.vercel.app
sourcestringneinz. B. vercel, v0, mcp

Response (200): success, data mit origin, updated, editorMessage, optional revalidation.

Öffentlich (Vercel Deploy Hook): POST {DASHBOARD_ORIGIN}/api/public/frontend-deployments/register mit Header X-Cockpit-Register-Token: frt_… (pro Center im Dashboard erzeugt) — kein centerId nötig, Center wird am Token erkannt.

MCP: cockpit_register_frontend_deployment — vorher optional cockpit_frontend_channels zur Verifikation.

Datensicherheit: Aktualisiert nur die URL des gewählten Kanals (websitePublicUrl / …) und frontendDeploymentMeta — löscht keine Inhalte und ersetzt keine Custom Domains.

Siehe v0 Deploy ans Cockpit melden.


Homepage-Kacheln & Seiten-Inhalte (MCP-Schreiben)

Bisher nur Dashboard oder öffentliches GET — ab AgencyOS v1 auch Schreiben per API-Key (mit Audit + Revalidate):

RessourceLesenSchreiben
Homepage-KachelnGET …/centers/{centerId}/homepage-tilesPOST (neu), PATCH …/homepage-tiles/{tileId}, DELETE …/homepage-tiles/{tileId}
Page ContentGET …/centers/{centerId}/page-content (optional ?pageType=)POST (Upsert pro pageType)

MCP: cockpit_homepage_tiles (action: list/create/update/delete), cockpit_page_content (action: list/get/upsert). Öffentliche Vorschau weiterhin cockpit_public_homepage_tiles / cockpit_public_page_content.

Gültige pageType-Werte: u. a. ueber-uns, kontakt, anfahrt, datenschutz, impressum, shops, jobs — vollständige Liste in apps/dashboard/src/lib/page-content-api.ts.


Suche (Jobs & Offices)

GET {DASHBOARD_ORIGIN}/api/agencyos/v1/searchcontentType unterstützt jetzt auch job und office (Komma-getrennt). MCP: cockpit_search_content mit gleichen Parametern.


Büros & Praxen (AgencyOS + MCP)

Was: CRUD für Offices/Praxen über AgencyOS v1 — Schreibfelder aligned mit Dashboard /api/offices (Partial-Update).

Warum: MCP/AgencyOS kannten bisher nur Basis-Felder; Logo, Bilder, Qualifikationen und Veröffentlichungszeiten fehlten.

Wer ist betroffen: Redaktion (Dashboard unverändert), Dev/Agenten (AgencyOS, MCP).

MethodePfadBeschreibung
GET/api/agencyos/v1/officesListe (centerId, q, type, status, limit)
POST/api/agencyos/v1/officesAnlegen — centerId, name, type oder officeTypeId
GET/api/agencyos/v1/offices/{officeId}Detail inkl. officeType, Center
PUT/api/agencyos/v1/offices/{officeId}Partial-Update — nur gesetzte Felder
DELETE/api/agencyos/v1/offices/{officeId}Archivieren (status: Archiviert)

Schreibbare Felder (Auszug): Stammdaten (name, type, status, specialty, description, …), Kontakt (phone, email, website, bookingUrl), Medien (logo, coverImage, teamPhoto, images), Team (owner, teamSize), JSON-Felder (openingHours, services, languages, qualifications, certifications, insuranceTypes, appointmentType, onlineServices), Barrierefreiheit/Parkplatz/Notfall, Flags (featured, verified), officeTypeId, publishStartDate, publishEndDate, displayId.

JSON-Felder: Text, JSON-String oder Array/Objekt — Serialisierung wie im Dashboard (apps/dashboard/src/lib/agencyos-office-fields.ts).

Workflow-Entwurf: POST mit submitToWorkflow: true → Content-Draft statt Direktanlage (wie bisher).

MCP: cockpit_list_offices, cockpit_get_office, cockpit_create_office, cockpit_update_office, cockpit_archive_office

So testen:

  1. GET /api/agencyos/v1/offices?centerId=… mit Bearer-Key.
  2. POST mit name, type, optional logo und qualifications: ["Facharzt"].
  3. PUT …/{officeId} mit featured: trueupdatedFields in der Antwort prüfen.
  4. MCP cockpit_get_office mit derselben officeId.

Teams-Benachrichtigungen (cockpitOS → AgencyOS, Phase 2)

Was: Wenn im Cockpit eine Social-Freigabe angefordert wird, kann das Dashboard optional einen HTTP-Hook an AgencyOS senden. AgencyOS liefert dann proaktive Teams-DMs oder Adaptive Cards an die gewählten Freigeber (Notification-Hub: team.cockpit-os.de).

Warum: Redaktion arbeitet im Cockpit; der Teams-Bot lebt in AgencyOS. Das CMS bleibt Content-Quelle und meldet Events — keine eigene Bot-Logik im Monorepo.

Wer ist betroffen: Redaktion/Freigeber (Teams), Dev/Betrieb (Env auf beiden Seiten).

Ablauf

cockpitOS (dieses Repo) — bereits vorbereitet

ThemaPfad
HTTP-Clientapps/dashboard/src/lib/integration/agencyos-teams-notify.ts
Hook nach Freigabe-Anfrageapps/dashboard/src/lib/integration/social-review-request-notify.ts
AuslöserapplySubmitReview in social-approval-actions.ts

Env (Dashboard, nur Server):

VariablePflichtBeschreibung
AGENCYOS_TEAMS_NOTIFY_URLja (für Aktivierung)Vollständige URL, z. B. https://team.cockpit-os.de/api/integrations/notifications/social-review
AGENCYOS_TEAMS_NOTIFY_SECRETja (für Aktivierung)Gemeinsames Geheimnis; Header Authorization: Bearer …

Ohne beide Variablen: kein Aufruf — Dashboard-Glocke, Desktop-Push und E-Mail laufen unverändert.

AgencyOS — zu implementieren (Notification-API)

POST {AGENCYOS_ORIGIN}/api/integrations/notifications/social-review

  • Auth: Authorization: Bearer <AGENCYOS_TEAMS_NOTIFY_SECRET> (gleicher Wert wie in Cockpit-Env)
  • Content-Type: application/json

Body (von Cockpit gesendet):

{
"source": "cockpitos",
"draftId": "cuid",
"centerId": "uuid",
"centerName": "Center XY",
"caption": "Post-Text…",
"requester": {
"userId": "cockpit-user-uuid",
"name": "Max Mustermann",
"email": "max@example.com"
},
"approvers": [
{ "userId": "uuid", "name": "Anna", "email": "anna@example.com" }
],
"approvalMode": "all",
"reviewUrl": "https://dashboard.cockpit-os.de/dashboard/social/approvals?highlight=…",
"guestReviewUrl": "https://dashboard.cockpit-os.de/freigabe/social/token (optional)"
}
FeldTypBeschreibung
source"cockpitos"Herkunftssystem
draftIdstringSocialPostDraft.id im Cockpit
approvalMode"all" | "one"Entspricht Cockpit-Freigaberegel
reviewUrlstringDeep-Link ins Cockpit-Freigabe-Board
guestReviewUrlstring | nullOptionaler Gast-Link ohne Login

User-Mapping: Cockpit-userId und AgencyOS-userId sind nicht identisch. AgencyOS soll Freigeber primär über email (oder teamsUserId / Entra aadObjectId) auflösen.

Erfolg (200):

{
"success": true,
"sent": { "teams": 2, "inApp": 1, "skipped": 0 }
}

Fehler: 401 ungültiges Secret, 422 fehlende Pflichtfelder.

Erwartetes Verhalten in AgencyOS:

  1. Pro Approver mit teamsConversationRef: Adaptive Card (Caption, Center, Freigeben-Link zum Cockpit)
  2. Ohne Bot-Kontakt: In-App-Notification + PWA-Push in AgencyOS (falls User dort existiert)
  3. Kein Blockieren des Cockpit-Workflows bei AgencyOS-Ausfall (Cockpit loggt nur)

So testen (End-to-End)

Schnellster Weg (Smoke-Test, ~30 Sekunden):

./scripts/smoke-agencyos-teams-notify.sh

Das Skript fragt Secret und Freigeber-E-Mail ab (oder aus Env / --load-env aus apps/dashboard/.env.local). Es prüft Auth (401) und sendet eine Test-Card an AgencyOS → Teams.

VariableWas du brauchst
AGENCYOS_TEAMS_NOTIFY_SECRETGleicher Wert in Cockpit und AgencyOS (Render)
TEST_APPROVER_EMAILE-Mail einer Person, die in AgencyOS existiert und den Teams-Bot einmal geöffnet hat

Vollständiger Flow (Cockpit-UI):

  1. AgencyOS: Notification-Endpunkt deployen + Secret setzen
  2. Cockpit Render: AGENCYOS_TEAMS_NOTIFY_URL + AGENCYOS_TEAMS_NOTIFY_SECRET setzen → Redeploy
  3. Freigeber: Teams-Bot in AgencyOS mindestens einmal öffnen (teamsConversationRef)
  4. Cockpit: Social-Post → Freigabe anfordern mit Test-Prüfer
  5. Prüfer erhält Teams-Nachricht; Cockpit-Glocke/E-Mail weiterhin wie bisher

Abgrenzung

SystemSocial-FreigabenTeams
cockpitOSSocialPostDraft, /dashboard/social/approvalsHook nur wenn Env gesetzt
AgencyOSeigener SocialPost / RedaktionsplanBot, Crons, Cards (MVP dort)

Social-Review-Outcome (Phase 2b)

Was: Nach Freigabe/Ablehnung/Publish-Fehler meldet Cockpit das Ergebnis an AgencyOS → Teams-DM an den Ersteller (nicht den Freigeber).

Warum: Der Anfragende erhält bewusst keine Freigabe-Anfrage-Benachrichtigung; das Outcome schließt den Kreis im Teams-Bot.

ThemaPfad
HTTP-Clientapps/dashboard/src/lib/integration/agencyos-teams-notify.ts (notifyAgencyOsTeamsSocialReviewOutcome)
AuslösernotifySocialReviewOutcome in social-review-notify.ts (nach Dashboard + E-Mail)

URL: Aus AGENCYOS_TEAMS_NOTIFY_URL abgeleitet (…/social-review…/social-review-outcome) oder explizit AGENCYOS_TEAMS_NOTIFY_OUTCOME_URL.

POST {AGENCYOS_ORIGIN}/api/integrations/notifications/social-review-outcome

  • Auth: gleiches AGENCYOS_TEAMS_NOTIFY_SECRET
  • Body:
{
"source": "cockpitos",
"draftId": "cuid",
"centerId": "uuid",
"centerName": "Center XY",
"caption": "Post-Text…",
"outcome": "approved",
"requester": { "userId": "…", "name": "…", "email": "sb@…" },
"reviewer": { "userId": "…", "name": "…", "email": "julia@…" },
"rejectionReason": null,
"reviewUrl": "https://dashboard.cockpit-os.de/dashboard/social/approvals?highlight=…"
}
outcomeBedeutung
approvedFreigegeben und Publish erfolgreich (oder geplant)
rejectedAbgelehnt — rejectionReason gesetzt
publish_failedFreigegeben, Outstand-Publish fehlgeschlagen

AgencyOS: Endpunkt + Card an requester.email (User-Match per E-Mail).

Social-Review-Digest (AgencyOS v1)

Was: Offene Cockpit-SocialPostDraft mit status=in_review, gruppiert nach Freigeber-E-Mail — für täglichen Digest-Cron in AgencyOS (neben native AgencyOS-Posts).

MethodePfad
GET/api/agencyos/v1/social-review-digest
GET/api/agencyos/v1/social-review-digest?approverEmail=julia@…

Auth: Agency-API-Key. MCP: cockpit_social_review_digest.

Response (Auszug):

{
"success": true,
"generatedAt": "2026-07-10T09:00:00.000Z",
"recipientCount": 3,
"totalOpenDrafts": 5,
"recipients": [
{
"userId": "uuid",
"name": "Julia",
"email": "julia@example.com",
"openCount": 2,
"items": [
{
"draftId": "cuid",
"centerName": "Burgaupark Jena",
"captionExcerpt": "Sommer-Sale…",
"requesterName": "Saad",
"approvalMode": "any",
"reviewUrl": "https://dashboard.cockpit-os.de/dashboard/social/approvals?highlight=…",
"guestReviewUrl": null
}
]
}
]
}

Smoke:

COCKPIT_AGENCYOS_API_KEY=sk_agencyos_… ./scripts/smoke-agencyos-social-review-digest.sh

In-Chat-Freigabe (AgencyOS Proxy)

Was: AgencyOS kann Freigabe/Ablehnung direkt aus der Teams-Card auslösen — Proxy zu Cockpit.

MethodePfad
POST/api/agencyos/v1/social/drafts/{draftId}/review

Body:

{
"action": "approve",
"approverEmail": "julia@example.com",
"reason": "optional bei reject"
}
  • Freigeber wird per E-Mail im Cockpit aufgelöst (muss aktiv sein und in assignedApproverIds stehen).
  • Gleiche Regeln wie Dashboard (all/any, Multi-Freigabe, Publish).
  • Kein MCP-Tool — nur serverseitiger AgencyOS-Proxy mit API-Key.

Implementierung: load-agencyos-social-review-digest.ts, social-review-digest/route.ts, social/drafts/[draftId]/review/route.ts.

Organization Accountability (ILG/HBB, „Wer ist zuständig?“)

Was: Aggregiert KAM-Zuständigkeiten und Org-Kontakte pro Organisation aus Cockpit — für Bot-Fragen wie „Wer ist für ILG zuständig?“.

MethodePfad
GET/api/agencyos/v1/organizations/{slugOrId}/accountability

slugOrId: Slug (ilg, hbb), Name oder UUID.

MCP: cockpit_organization_accountability

Response (Auszug):

{
"success": true,
"data": {
"organization": { "id": "…", "name": "ILG", "slug": "ilg" },
"centerCount": 27,
"centersWithKam": 1,
"keyAccountManagers": [
{
"userId": "…",
"name": "Julia Warns",
"email": "juw@schickma.de",
"assignmentKind": "primary",
"centerCount": 1,
"centers": [{ "centerId": "…", "centerName": "Burgaupark Jena" }]
}
],
"hasAccountability": true,
"hints": ["Haupt-KAM laut Cockpit: Julia Warns (juw@schickma.de)", "26 von 27 Centern ohne KAM-Zuweisung im Cockpit"]
}
}

Hinweis: AgencyOS search_centers nutzt heute nur Customer.accountManagerId — ohne Cockpit-Fallback bleibt die Bot-Antwort leer, obwohl Cockpit-Daten existieren können. AgencyOS soll bei leerem accountManager diese API nachladen.

Pfad Cockpit: load-organization-accountability.ts, organizations/[slugOrId]/accountability/route.ts

Organisations-KAM mit Vererbung (2026-07-10)

Was: KAMs können einmal pro Organisation gesetzt werden und gelten für alle Center — außer ein Center hat eigene KAM-Zuweisung (Abweichung).

EbeneDashboardAPI
Organisation/dashboard/organizations/{slug} — KAM-KarteGET/PUT /api/organizations/{slug}/key-account-managers
Center (Abweichung)Center-Profil → Tab TeamGET/PUT /api/centers/{id}/key-account-managersrevertToOrganization: true setzt Vererbung zurück
AgencyOSGET/PUT …/organizations/{slugOrId}/key-account-managers, GET/PUT …/centers/{centerId}/key-account-managers (liefert source, hasCenterOverride)

Vererbungsregel: Keine Center-Zeilen → Organisations-KAMs. Mindestens eine Center-Zeile → nur Center-KAMs.

MCP lesen: cockpit_center_key_account_managers, cockpit_organization_key_account_managers, cockpit_organization_accountability, cockpit_kam_briefing_index, cockpit_kam_quality_watch_index

MCP schreiben: cockpit_set_center_key_account_managers, cockpit_set_organization_key_account_managers — Body wie Dashboard-PUT (userIds[], assignments[], Center: revertToOrganization: true). User-UUIDs über cockpit_center_team oder Nutzer-Verwaltung.

Noch ohne AgencyOS/MCP: KAM-Zuweisung aus der Nutzer-Bearbeitung (kamCenterAssignments am User-Endpoint).


Externe Ansprechpartner (Center)

Was: Agentur, Haustechnik, Sicherheit usw. pro Center — Tab Team & Ansprechpartner im Dashboard; Speicherung in CenterContact mit metadata.contactKind = external.

Warum: KI-Kontext, Notfall-Listen und AgencyOS-Automation brauchen strukturierte externe Kontakte — bisher nur Dashboard ohne MCP.

MethodePfadBeschreibung
GET/api/agencyos/v1/centers/{centerId}/external-contactsListe
POST/api/agencyos/v1/centers/{centerId}/external-contactsAnlegen
PUT/api/agencyos/v1/centers/{centerId}/external-contacts/{contactId}Aktualisieren
DELETE/api/agencyos/v1/centers/{centerId}/external-contacts/{contactId}Löschen

Body (POST/PUT): name (Pflicht), optional company, role, email, phone, type (Haustechnik, …), emergency, responseTime, notes.

MCP: cockpit_center_external_contacts, cockpit_create_center_external_contact, cockpit_update_center_external_contact, cockpit_delete_center_external_contact

Migration: packages/database/migrations/20260722_center_contact_metadata_SAFE.sql — Spalte metadata (JSONB), email nullable.

So testen: Dashboard Center → Team → Kontakt anlegen → MCP GET mit gleicher centerId → Eintrag in data[].


Website-Ansprechpartner (Center Website → Kontakte)

Was: Center Manager und weitere Ansprechpartner für Kontaktseite / Impressum — Tab Center Website → Grundeinstellungen → Kontakte (CenterContact mit metadata.contactKind = website).

Abgrenzung: Nicht externe Team-Kontakte (Agentur, Haustechnik) — die liegen unter Center → Team & Ansprechpartner (external-contacts).

MethodePfadBeschreibung
GET/api/agencyos/v1/centers/{centerId}/contactsListe (Auth)
POST/api/agencyos/v1/centers/{centerId}/contactsAnlegen
PUT/api/agencyos/v1/centers/{centerId}/contacts/{contactId}Aktualisieren
DELETE/api/agencyos/v1/centers/{centerId}/contacts/{contactId}Löschen
GET/api/centers/{centerId}/contacts/publicÖffentlich (v0, nur aktive)

Body (POST/PUT): role + name (Pflicht), optional email, phone, department, photoUrl, displayOrder, isActive.

MCP lesen: cockpit_center_website_contacts, cockpit_public_center_contacts (v0)

MCP schreiben: cockpit_create_center_website_contact, cockpit_update_center_website_contact, cockpit_delete_center_website_contact

v0: GET …/public-visitor-surfaceapiHints.contactsPublicGet

So testen: Dashboard Center Website → Kontakt anlegen → cockpit_center_website_contacts → gleicher Eintrag; v0: cockpit_public_center_contacts.



Operational Briefing (Center-KAM-Daily, Phase 2)

Was: Liefert pro Center die operativen To-dos aus dem Cockpit — offene Social-Postings, Google-Rezensionen ohne Antwort, Reporting-Erinnerungen, Website-Workflow-Entwürfe. AgencyOS nutzt die API für tägliche Teams-DMs an KAMs (Cron center-kam-briefing).

Warum: KAM-Daten (Tasks) liegen in AgencyOS; Social/Reviews/Reporting in Cockpit — eine Aggregat-API vermeidet Duplikate.

Endpunkte

MethodePfadBeschreibung
GET/api/agencyos/v1/centers/{centerId}/operational-briefingEin Center
GET/api/agencyos/v1/operational-briefing?centerIds=id1,id2Bulk (max. 30, nur erlaubte Center)

Auth: Authorization: Bearer <sk_agencyos_…> (gleicher Key wie andere v1-Routen)

MCP: cockpit_operational_briefing — Parameter centerId oder centerIds (kommagetrennt).

Beispiel-Response (data)

{
"centerId": "uuid",
"centerName": "Rathaus-Galerie Wuppertal",
"generatedAt": "2026-07-08T20:00:00.000Z",
"social": {
"pending": 1,
"inReview": 2,
"totalOpen": 3,
"href": "https://dashboard.cockpit-os.de/dashboard/social/approvals?status=in_review"
},
"workflow": { "openDrafts": 0, "href": "https://dashboard.cockpit-os.de/dashboard/workflow" },
"reviews": {
"configured": true,
"available": true,
"unanswered": 3,
"responseNeeded": 1,
"href": "https://dashboard.cockpit-os.de/dashboard/analytics/reviews?centerId=…"
},
"reporting": {
"configured": true,
"reminderMonth": "2026-06",
"monthNotSent": true,
"quarterReminder": false,
"href": "https://dashboard.cockpit-os.de/dashboard/analytics/client-report?centerId=…"
},
"hints": [
"Social: 2 in Prüfung, 1 ausstehend",
"Google: 3 Rezensionen ohne Antwort (1 mit Handlungsbedarf)",
"Reporting: Monatsbericht Juni 2026 noch nicht versendet"
],
"hasActionItems": true
}

Reporting-Regeln (Cockpit)

FlagBedeutung
monthNotSentVormonat (reminderMonth) hat keinen Eintrag in client_report_notify_log und Center hat clientReportNotifyEmail
quarterReminderErste 14 Tage von Jan/Apr/Jul/Okt und monthNotSent

Smoke-Test (Cockpit-Repo)

COCKPIT_AGENCYOS_API_KEY=sk_agencyos_… CENTER_ID=uuid ./scripts/smoke-agencyos-operational-briefing.sh

Implementierung (Cockpit)

PfadRolle
apps/dashboard/src/lib/integration/load-agencyos-operational-briefing.tsAggregat-Logik
…/centers/[centerId]/operational-briefing/route.tsEinzel-Center
…/operational-briefing/route.tsBulk

AgencyOS implementiert Cron + Teams-Nachricht — siehe Prompt in Repo-Doku / separater Agent-Chat.


KAM-Zuordnung (Cockpit) & Briefing-Index

Was: Key Account Manager werden im Cockpit pro Center gepflegt — mehrere Personen möglich (z. B. Urlaubsvertretung). AgencyOS nutzt die E-Mail-Adresse für Teams-DMs; die operative Datenaggregation bleibt in operational-briefing.

Warum: KAM war bisher nur in AgencyOS (keyAccountManagerId, ein User). Cockpit ist die führende Quelle für Center-Stammdaten und operative To-dos.

Dashboard (Redaktion / Betrieb)

WoPfad
UICenter-Detail → Tab Team & Ansprechpartner → Karte Key Account Manager (KAM)
API (Session)GET/PUT /api/centers/{centerId}/key-account-managers — Body PUT siehe JSON-Beispiele unten

Body PUT (Session-API):

{ "userIds": ["uuid-1", "uuid-2"] }
{ "assignments": [{ "userId": "uuid-1", "assignmentKind": "primary" }] }
{ "revertToOrganization": true }

Wer: SUPER_ADMIN, CENTER_ADMIN, ORG_MARKETING_MANAGER (Schreiben); Lesen für alle mit Center-Zugang.

MCP: Lesen cockpit_center_key_account_managers · Schreiben cockpit_set_center_key_account_managers · Org: cockpit_organization_key_account_managers / cockpit_set_organization_key_account_managers

So testen:

  1. Center öffnen → Profil → Team.
  2. Einen oder mehrere aktive Cockpit-User als KAM hinzufügen.
  3. GET /api/agencyos/v1/centers/{centerId}/key-account-managers mit AgencyOS-Key — gleiche Liste.
  4. PUT mit gleichem Key + userIds oder MCP cockpit_set_center_key_account_managers.
  5. GET /api/agencyos/v1/kam-briefing-index — Empfänger gruppiert nach KAM inkl. Briefings.

AgencyOS-v1-Endpunkte

MethodePfadBeschreibung
GET/api/agencyos/v1/centers/{centerId}/key-account-managersEffektive KAM-Liste (inkl. Vererbung)
PUT/api/agencyos/v1/centers/{centerId}/key-account-managersCenter-KAMs setzen oder revertToOrganization: true
GET/api/agencyos/v1/organizations/{slugOrId}/key-account-managersOrganisations-KAMs
PUT/api/agencyos/v1/organizations/{slugOrId}/key-account-managersOrganisations-KAMs setzen
GET/api/agencyos/v1/organizations/{slugOrId}/hostingShared Vercel: vercelProjectsByTemplate, Fallback vercelSharedProjectId
PATCH/api/agencyos/v1/organizations/{slugOrId}/hostingMap pflegen (nur Global-API-Key); Body: vercelProjectsByTemplate, optional vercelSharedProjectId
GET/api/agencyos/v1/organizations/{slugOrId}/bulk-center-hosting?websiteTemplate=…Übersicht Hosting aller Center (optional Template-Filter)
POST/api/agencyos/v1/organizations/{slugOrId}/bulk-center-hostingBulk: Center websiteHostingChannel + vercelProjectMode + optional Org-Map merge
PATCH/api/agencyos/v1/centers/{centerId}/hostingEinzelnes Center: Hosting-Kanal + Vercel-Modus
GET/api/agencyos/v1/kam-briefing-indexAlle KAM-Empfänger + Briefings (gruppiert nach User)
GET/api/agencyos/v1/kam-briefing-index?onlyWithActionItems=trueNur Empfänger mit mindestens einem Center mit hasActionItems

operational-briefing enthält zusätzlich kams[] pro Center:

"kams": [
{
"userId": "uuid",
"name": "Max Mustermann",
"email": "max@example.com",
"sortOrder": 0,
"assignmentKind": "primary"
},
{
"userId": "uuid2",
"name": "Julia Vertretung",
"email": "julia@agency.de",
"sortOrder": 1,
"assignmentKind": "substitute"
}
]

Smoke-Tests (Cockpit-Repo)

COCKPIT_AGENCYOS_API_KEY=sk_agencyos_… ./scripts/smoke-agencyos-kam-briefing-index.sh
ONLY_WITH_ACTION_ITEMS=true COCKPIT_AGENCYOS_API_KEY=… ./scripts/smoke-agencyos-kam-briefing-index.sh

Implementierung (Cockpit)

PfadRolle
packages/database/migrations/20260708233000_add_center_key_account_managers_SAFE.sqlTabelle
apps/dashboard/src/lib/center-key-account-managers.tsLade-/Speicher-Logik, Briefing-Index
…/api/centers/[centerId]/key-account-managers/route.tsDashboard-API
…/api/agencyos/v1/centers/[centerId]/key-account-managers/route.tsAgencyOS-Read
…/api/agencyos/v1/kam-briefing-index/route.tsCron-Einstieg für Teams-DMs
apps/dashboard/src/components/center-kam-assignments-card.tsxUI

AgencyOS: Cron center-kam-briefing soll kam-briefing-index konsumieren (nicht mehr ShoppingCenter.keyAccountManagerId als Primärquelle).

MCP: cockpit_center_key_account_managers, cockpit_kam_briefing_index, cockpit_operational_briefing (ein Center oder Bulk centerIds).


Content-Quality-Watch (Variante A)

Was: Liefert pro Center Qualitäts-Hinweise für den AgencyOS Teams-Bot — fehlende Center-/Shop-Stammdaten, Inhalte ohne Titelbild, Inhaltslücken (Mengen) und lange Inaktivität ohne Veröffentlichung. Getrennt vom operativen Tages-Briefing.

Warum: Proaktive KAM-Nachrichten wie „8 Shops ohne Logo“ oder „seit 6 Wochen nichts veröffentlicht“ brauchen eine Aggregat-API — nicht 30 Einzelabfragen pro Center.

Endpunkte

MethodePfadBeschreibung
GET/api/agencyos/v1/centers/{centerId}/content-quality-watchEin Center
GET/api/agencyos/v1/content-quality-watch?centerIds=id1,id2Bulk (max. 30)
GET/api/agencyos/v1/content-quality-watch?…&onlyWithIssues=trueBulk nur mit hasQualityIssues
GET/api/agencyos/v1/kam-quality-watch-indexNach KAM gruppiert
GET/api/agencyos/v1/kam-quality-watch-index?onlyWithIssues=trueNur KAMs mit Qualitätsproblemen

Query-Parameter (optional): inactiveDays (21), minOffers (3), minEvents (1), minNews (1), minJobs (0), maxShopSamples (5)

Auth: Authorization: Bearer <sk_agencyos_…>

MCP: cockpit_content_quality_watch, cockpit_kam_quality_watch_index

Beispiel-Response (data)

{
"centerId": "uuid",
"centerName": "Rathaus-Galerie Wuppertal",
"hasQualityIssues": true,
"qualityScore": 62,
"qualityHints": [
"Stammdaten: Center ohne Öffnungszeiten",
"Shops: 8 von 142 ohne Logo (u.a. H&M, Zara, …)",
"Inhalte: 3 aktive Angebote ohne Titelbild",
"Aktivität: seit 42 Tagen nichts veröffentlicht"
],
"issues": {
"center": { "missingLogo": false, "missingCoverImage": false, "missingHeroImage": true, "missingOpeningHours": true },
"shops": { "activeTotal": 142, "withoutLogo": 8, "withoutCoverImage": 12, "withoutOpeningHours": 5, "samples": [] },
"content": { "activeOffersWithoutImage": 3, "activeEventsWithoutImage": 0, "publishedNewsWithoutImage": 1, "gaps": [] },
"activity": { "lastPublishedAt": "2026-05-28T10:00:00.000Z", "daysSinceLastPublish": 42, "inactiveThresholdDays": 21, "isInactive": true }
},
"links": {
"shops": "https://dashboard.cockpit-os.de/dashboard/content/centers/…?tab=shops",
"centerEdit": "https://dashboard.cockpit-os.de/dashboard/content/centers/…?tab=team",
"content": "https://dashboard.cockpit-os.de/dashboard/content/centers/…"
},
"kams": [{ "userId": "…", "name": "…", "email": "…", "assignmentKind": "primary" }]
}

Smoke-Test (Cockpit-Repo)

COCKPIT_AGENCYOS_API_KEY=sk_agencyos_… CENTER_ID=uuid ./scripts/smoke-agencyos-content-quality-watch.sh

Der Smoke-Test prüft Einzel-Center und kam-quality-watch-index?onlyWithIssues=true.

Implementierung (Cockpit)

PfadRolle
apps/dashboard/src/lib/integration/load-agencyos-content-quality-watch.tsAggregat-Logik
…/centers/[centerId]/content-quality-watch/route.tsEinzel-Center
…/content-quality-watch/route.tsBulk
…/kam-quality-watch-index/route.tsKAM-Index für Cron
apps/dashboard/src/lib/center-key-account-managers.tsloadKamQualityWatchIndex

AgencyOS: Cron center-quality-watch + Bot-Tools — separater Agent-Chat im SMG-AgencyOS-Repo.


OpenAPI (Swagger) – optional nutzen

Im Repository liegt eine maschinenlesbare Spezifikation:

Empfehlung:

  • Markdown (diese Seite) bleibt die verständliche Anleitung inkl. Ablauf und Sicherheit.
  • OpenAPI lohnt sich, wenn ihr Client-Code generieren, Contract-Tests oder Swagger UI (z. B. editor.swagger.io mit Import-URL) nutzen wollt.
  • Nachteil: Zwei Quellen – bei API-Änderungen OpenAPI und diese Seite pflegen, oder langfristig die Beschreibung aus OpenAPI in die Docs einbinden (Plugin/Aufwand).

Kurz: Swagger/OpenAPI ist sinnvoll, aber nicht Pflicht. Für AgencyOS reicht zunächst diese Doku; OpenAPI ist ein komfortables Zusatzangebot.


Implementierung im Repo (Referenz)

ThemaPfad
Magic Link GET/POSTapps/dashboard/src/app/api/agencyos/magic-link/route.ts
Completeapps/dashboard/src/app/api/agencyos/magic-link/complete/route.ts
Organisationen (Session)apps/dashboard/src/app/api/agencyos/organizations/route.ts
Connect-UIapps/dashboard/src/app/agencyos/connect/page.tsx
v1 Centersapps/dashboard/src/app/api/agencyos/v1/centers/route.ts, …/v1/centers/[centerId]/route.ts
v1 Center-Kontext (KI)apps/dashboard/src/app/api/agencyos/v1/centers/[centerId]/context/route.ts
Ladelogik Kontext (Shops/Services)apps/dashboard/src/lib/integration/load-agencyos-center-context.ts
v1 Content Pushapps/dashboard/src/app/api/agencyos/v1/content/push/route.ts
v1 Push-Vorschau (dry-run)apps/dashboard/src/app/api/agencyos/v1/content/push/preview/route.ts
v1 Content-Entwürfe (Liste, Detail, Touchpoint)apps/dashboard/src/app/api/agencyos/v1/drafts/route.ts, …/drafts/[draftId]/route.ts, …/drafts/[draftId]/customer-touchpoint-suggestion/route.ts
v1 Audit-Logapps/dashboard/src/app/api/agencyos/v1/audit-logs/route.ts
v1 Center-Teamapps/dashboard/src/app/api/agencyos/v1/centers/[centerId]/team/route.ts
v1 Website-Config…/centers/[centerId]/website-config/route.ts (GET + PUT)
v1 Outstand-Kanal-Inventar (Bulk)apps/dashboard/src/app/api/agencyos/v1/social/center-accounts/route.ts, outstand-center-inventory.ts
v1 Website-Reiter-Schema…/website-config-schema/route.ts, website-config-mcp-schema.ts, mcp-tab-hints-shared.ts, mcp-tab-hints-all-templates.ts, ilg-rgw-mcp-tab-fields.ts
v1 Homepage-Kacheln…/centers/[centerId]/homepage-tiles/route.ts, …/homepage-tiles/[tileId]/route.ts
v1 Page Content…/centers/[centerId]/page-content/route.ts
v1 Mediathek (Liste)apps/dashboard/src/app/api/agencyos/v1/media/route.ts
v1 Sucheapps/dashboard/src/app/api/agencyos/v1/search/route.ts
Push → Revalidateapps/dashboard/src/lib/integration/agencyos-content-revalidate.ts
Push-Audit (Integration)apps/dashboard/src/lib/integration/integration-audit.ts, process-center-entity-push.ts
Gemeinsame Push-Logik (mit WordPress geteilt)apps/dashboard/src/lib/integration/process-center-entity-push.ts
Teams-Hook Social-Freigabe (Phase 2)apps/dashboard/src/lib/integration/agencyos-teams-notify.ts, social-review-request-notify.ts
Operational Briefing (KAM-Daily)load-agencyos-operational-briefing.ts, …/operational-briefing/route.ts
KAM-Zuordnung & Briefing-Indexcenter-key-account-managers.ts, …/key-account-managers/route.ts, …/kam-briefing-index/route.ts

Verwandte Dokumentation

Nutzungsstatistik: Seitenaufrufe werden anonymisiert erfasst. Im Umami-Dashboard nach diesem Pfad filtern: /developer-guide/api-agencyos-integration