{
  "openapi": "3.1.0",
  "info": {
    "title": "campaignKit Public API",
    "version": "1.0.0",
    "description": "Einheitliche, versionierte API zwischen Kundenplattformen und der Meta Marketing API. Der mitgeschickte API-Key bestimmt den Meta-Account; der Meta-Token verlässt niemals den Server.\nDiese Spec wird roh unter `/docs/openapi.yaml` und `/docs/openapi.json` für Tooling (Postman, Codegen) ausgeliefert. Die menschenlesbare Docs-Seite und das `llms.txt`-Bündel stammen aus `lib/docs-data.ts` (separat gepflegt). Beide Quellen müssen synchron bleiben; der Drift-Wächter `lib/docs-sync.test.ts` vergleicht die Endpunkt-Mengen und schlägt bei Abweichung an.\nKampagnen werden ausschließlich im Admin-Dashboard angelegt (kein POST über die API). Über die API werden Medien, Creatives, Ad-Sets und Ads erstellt; Optimierung, Placements, Geo, Budget-Obergrenze und DSA sind durch die Kampagnen-Konfiguration vorgegeben und können nicht überschrieben werden. Reporting, Webhooks und Audiences (Custom/Lookalike) sind verfügbar. Conversion-Events sendet die Kundenplattform selbst per Pixel/CAPI direkt an Meta, daher gibt es dafür keinen API-Endpunkt.\n"
  },
  "servers": [
    {
      "url": "/api/v1",
      "description": "Versioniert unter /api/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "System",
      "description": "Verbindung & Health."
    },
    {
      "name": "Media",
      "description": "Bild-Upload (liefert image_hash für Creatives)."
    },
    {
      "name": "Creatives",
      "description": "Ad-Creatives (Bild + externe Landingpage)."
    },
    {
      "name": "AdSets",
      "description": "Ad-Sets unter einer (im Dashboard angelegten) Kampagne."
    },
    {
      "name": "Ads",
      "description": "Ads (Ad-Set + Creative)."
    },
    {
      "name": "Templates",
      "description": "Zugewiesene Vorlagen + Parameter-Schema (Vorlage für KI-Inhalte)."
    },
    {
      "name": "Targeting",
      "description": "Lookup-Helfer für Geo- und Interessen-Keys."
    },
    {
      "name": "Status",
      "description": "Live-Status einer Ad (effective_status + Ablehnungsgrund)."
    },
    {
      "name": "Webhooks",
      "description": "Status-Webhooks (Registrierung; Push bei ad.status_changed)."
    },
    {
      "name": "Reporting",
      "description": "Insights & Reporting (Spend, Conversions, ROAS, Breakdowns)."
    },
    {
      "name": "Audiences",
      "description": "Custom-/Lookalike-Audiences (Kontakte werden serverseitig gehasht, keine PII at rest)."
    },
    {
      "name": "Plan",
      "description": "Vertragszustand, voraussichtliche Gebühr, Deaktivieren und Rechnungs-Portal (rückwirkende Stufen-Abrechnung)."
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "System"
        ],
        "summary": "Authentifizierter Health-Check",
        "description": "Prüft, dass der API-Key gültig ist und die API erreichbar. Erfordert mindestens den Scope `reporting`.\n",
        "responses": {
          "200": {
            "description": "API erreichbar.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "request_id"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "status",
                        "api_version"
                      ],
                      "properties": {
                        "status": {
                          "type": "string",
                          "example": "ok"
                        },
                        "api_version": {
                          "type": "string",
                          "example": "v1"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/media/images": {
      "post": {
        "operationId": "uploadMediaImage",
        "tags": [
          "Media"
        ],
        "summary": "Bild von URL hochladen",
        "description": "Übergibt ein Bild per öffentlich erreichbarer https-URL. Unser Server lädt die Datei und reicht die Bytes UNVERÄNDERT (keine Kompression) an Meta weiter; zurück kommt der `image_hash` für Creatives. Synchron. Die URL wird validiert (nur https, keine internen Ziele, keine Redirects). Mit unserem Supabase: signierte URL via createSignedUrl(path, 3600). Ohne Supabase: beliebige öffentlich erreichbare https-URL.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Direkte https-URL zur fertigen Bilddatei (JPG/PNG/WebP, ≤ 30 MB), von unserem Backend abrufbar, ohne Weiterleitung."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Bild an Meta übergeben.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "image_hash": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      },
      "get": {
        "operationId": "listMediaImages",
        "tags": [
          "Media"
        ],
        "summary": "Bilder aus Meta auflisten",
        "description": "Listet die Ad-Images des Werbekontos direkt aus Meta. Da Konto und Kunde 1:1 zugeordnet sind, ist das die Bild-Mediathek des Kunden. Optional per `hash` auf ein einzelnes Bild gefiltert. Scope `reporting` genügt.\n",
        "parameters": [
          {
            "in": "query",
            "name": "hash",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Auf einen einzelnen image_hash filtern."
          }
        ],
        "responses": {
          "200": {
            "description": "Liste der Ad-Images aus Meta.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "image_hash": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "nullable": true
                          },
                          "width": {
                            "type": "integer",
                            "nullable": true
                          },
                          "height": {
                            "type": "integer",
                            "nullable": true
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "created_time": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/media/videos": {
      "post": {
        "operationId": "uploadMediaVideo",
        "tags": [
          "Media"
        ],
        "summary": "Video von URL hochladen",
        "description": "Übergibt ein Video per öffentlich erreichbarer https-URL. Anders als beim Bild lädt META die Datei selbst (file_url) — die Bytes laufen nie durch uns, daher sind große bis 4K-Videos möglich. Zurück kommt die `video_id` und ein Status (meist `PROCESSING`); asynchron, daher über GET /media/videos/{id} pollen, bis `READY`. Mit unserem Supabase: signierte URL via createSignedUrl(path, 3600) (Gültigkeit ≥ 1 h, da Meta sie abruft). Ohne Supabase: beliebige öffentlich erreichbare https-URL.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Direkte https-URL zum fertigen Video (MP4/MOV), von Metas Servern abrufbar."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Video an Meta übergeben, Verarbeitung läuft asynchron.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "video_id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "PROCESSING",
                            "READY",
                            "FAILED"
                          ]
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      },
      "get": {
        "operationId": "listMediaVideos",
        "tags": [
          "Media"
        ],
        "summary": "Videos aus Meta auflisten",
        "description": "Listet die Ad-Videos des Werbekontos direkt aus Meta (video_id, Titel, Status, Länge, Erstellzeit). Da Konto und Kunde 1:1 zugeordnet sind, ist das die Video-Mediathek des Kunden. Scope `reporting` genügt.\n",
        "responses": {
          "200": {
            "description": "Liste der Ad-Videos aus Meta.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "video_id": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string",
                            "nullable": true
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "PROCESSING",
                              "READY",
                              "FAILED"
                            ]
                          },
                          "length": {
                            "type": "integer",
                            "nullable": true
                          },
                          "created_time": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/media/videos/{id}": {
      "get": {
        "operationId": "getMediaVideoStatus",
        "tags": [
          "Media"
        ],
        "summary": "Video-Verarbeitungsstatus pollen",
        "description": "Liefert den aktuellen Verarbeitungsstatus eines Videos: `PROCESSING`, `READY` oder `FAILED`. `READY` = im Creative nutzbar. Scope `reporting` genügt.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Interne Video-Asset-ID aus POST /media/videos."
          }
        ],
        "responses": {
          "200": {
            "description": "Aktueller Verarbeitungsstatus.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "video_id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "PROCESSING",
                            "READY",
                            "FAILED"
                          ]
                        },
                        "processed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/templates": {
      "get": {
        "operationId": "listTemplates",
        "tags": [
          "Templates"
        ],
        "summary": "Zugewiesene Vorlagen auflisten",
        "description": "Listet die dem Kunden zugewiesenen, aktiven Vorlagen (UUID, Name, Beschreibung, `type` = `image`/`video`). Je Vorlage zusätzlich `preview_urls` (je Format die URL des Vorschau-Bildes) und `has_params` (ob für diese Vorlage Vorschau-Werte hinterlegt sind). Das Auflisten rendert nicht; die Bilder werden erst beim Abruf der jeweiligen preview_url lazy gerendert und danach gecacht. Die Parameter pflegt der Betreiber je Kunde im Admin-Dashboard. Das Parameter-Schema einer Vorlage liefert GET /templates/{id}. Scope `reporting` genügt.\n",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste der zugewiesenen Vorlagen.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/PublicTemplateSummary"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "has_params": {
                                "type": "boolean",
                                "description": "True, wenn für diese Vorlage Vorschau-Werte hinterlegt sind."
                              },
                              "preview_urls": {
                                "$ref": "#/components/schemas/TemplatePreviewUrls"
                              }
                            }
                          }
                        ]
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/templates/{id}": {
      "get": {
        "operationId": "getTemplate",
        "tags": [
          "Templates"
        ],
        "summary": "Parameter-Schema einer Vorlage (für KI)",
        "description": "Liefert das einheitliche Erklärungs-Schema einer zugewiesenen, aktiven Vorlage: je Parameter Name, Typ, Pflicht/optional, Bezeichnung, Beschreibung (Hinweis für die Inhaltserstellung), Min-/Max-Zeichen und ein Beispiel. Nur zugewiesene Vorlagen — sonst 404. Scope `reporting`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Vorlagen-Schema.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PublicTemplateDetail"
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/templates/{id}/preview": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "getTemplatePreviewParams",
        "tags": [
          "Templates"
        ],
        "summary": "Hinterlegte Vorschau-Parameter lesen",
        "description": "Liefert die für diese Vorlage hinterlegten Parameter-Werte (vom Betreiber je Kunde im Admin-Dashboard gepflegt; leeres Objekt, wenn noch nichts gesetzt) plus die preview_urls je Format. Scope `reporting`.\n",
        "responses": {
          "200": {
            "description": "Gespeicherte Werte + Vorschau-URLs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TemplatePreviewParams"
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/templates/{id}/preview.png": {
      "get": {
        "operationId": "getTemplatePreviewImage",
        "tags": [
          "Templates"
        ],
        "summary": "Vorschau-Bild (öffentlich, direkt einbettbar)",
        "security": [],
        "description": "Öffentlicher Bild-Endpunkt, direkt im Browser einbettbar (img-Tag) ohne API-Key. Autorisiert über die signierten Query-Parameter `c` (Kunde) und `k` (Lese-Token), die bereits in den preview_urls aus GET /templates enthalten sind — die preview_url wird unverändert verwendet, nicht selbst gebaut. Liefert das Bild gefüllt mit den vom Betreiber hinterlegten Parametern (sonst den Beispiel-Werten der Vorlage) als 302-Redirect auf eine öffentliche, unratbare Bild-URL (Bytes via Storage-CDN). Das Bild wird vorab gerendert und content-addressiert gecacht (spätestens beim ersten Abruf); nach einer Parameter- oder Vorlagen-Änderung beim nächsten Abruf einmalig neu erzeugt. Der Token ist stabil und läuft nicht ab. Ungültiger/fehlender Token oder unbekannte/entzogene Vorlage → 404.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "c",
            "required": true,
            "description": "Kunden-Kennung (Teil der Signatur, in der preview_url enthalten).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "k",
            "required": true,
            "description": "Signierter Lese-Token (in der preview_url enthalten).",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "ratio",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1:1",
                "4:5",
                "9:16"
              ],
              "default": "1:1"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect auf die Bild-URL (Header `Location`).",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/templates/render": {
      "post": {
        "operationId": "renderTemplate",
        "tags": [
          "Templates"
        ],
        "summary": "Vorlage rendern (Creative-Entwurf, nur Visual)",
        "description": "Rendert eine aktive Vorlage mit den übergebenen `params` in die drei Formate 1:1, 4:5 und 9:16 und legt einen Creative-ENTWURF an. Hier entsteht nur das **Visual** — die Anzeigen-Copy (Primärer Text, Überschrift, Beschreibung, Call-to-Action, Ziel-Link, angezeigter Link) gehört zur Anzeige und wird in POST /ads gesetzt; dort wird aus diesem Entwurf das finale Creative gebaut. Die zurückgegebene `id` ist die `creative_id` für POST /ads — die Antwort enthält noch KEINE `meta_creative_id`. Enthält die Vorlage einen Video-Layer, läuft das Rendern asynchron (Antwort 202, Status PROCESSING). Param-gebundene Bild-/Logo-URLs werden vor dem Abruf gegen interne Ziele geprüft (SSRF-Schutz) und große externe Bilder vorab verkleinert. Scope `full`, idempotent (Header `Idempotency-Key`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "template"
                ],
                "properties": {
                  "template": {
                    "type": "string",
                    "format": "uuid",
                    "description": "UUID der Vorlage (nur aktive Vorlagen werden akzeptiert)."
                  },
                  "params": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Key/Value je Vorlagen-Parameter (siehe GET /templates/{id}). Bild-URLs akzeptieren JPG, PNG oder WebP; ein nicht erreichbares oder ungültiges Bild wird mit 422 abgelehnt (nicht still weggelassen)."
                  },
                  "name": {
                    "type": "string",
                    "description": "Optionaler interner Name des erzeugten Creatives.",
                    "example": "Sommer-Sale · 1080"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Vorschau erzeugt (ohne `link`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "template": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "IMAGE",
                            "VIDEO"
                          ]
                        },
                        "status": {
                          "type": "string",
                          "example": "PREVIEW"
                        },
                        "formats": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string",
                            "format": "uri"
                          },
                          "description": "Je Format (1:1, 4:5, 9:16) die Bild-URL (nur IMAGE)."
                        },
                        "overlays": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string",
                            "format": "uri"
                          },
                          "description": "Je Format die transparente Overlay-PNG-URL (nur VIDEO)."
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Bild-Creative finalisiert (mit `link`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "meta_creative_id": {
                          "type": "string"
                        },
                        "template": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "type": {
                          "type": "string",
                          "example": "IMAGE"
                        },
                        "formats": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string",
                            "format": "uri"
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Video-Render-Job angelegt (mit `link`); Creative-Status PROCESSING.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "type": {
                          "type": "string",
                          "example": "VIDEO"
                        },
                        "status": {
                          "type": "string",
                          "example": "PROCESSING"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/templates/render/{id}": {
      "get": {
        "operationId": "getTemplateRenderStatus",
        "tags": [
          "Templates"
        ],
        "summary": "Status eines (Video-)Render-Auftrags",
        "description": "Liefert den Status eines asynchronen Video-Renders: PROCESSING (Compositing/Upload läuft), READY (`meta_creative_id` vorhanden, in POST /ads nutzbar) oder FAILED (`error` gesetzt). `{id}` ist die Creative-ID aus dem 202-Response von POST /templates/render. Reiner DB-Read. Scope `reporting`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Render-Status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "PROCESSING",
                            "READY",
                            "FAILED"
                          ]
                        },
                        "meta_creative_id": {
                          "type": "string",
                          "nullable": true
                        },
                        "video_id": {
                          "type": "string",
                          "nullable": true
                        },
                        "output_url": {
                          "type": "string",
                          "format": "uri",
                          "nullable": true
                        },
                        "error": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/creatives": {
      "get": {
        "operationId": "listCreatives",
        "tags": [
          "Creatives"
        ],
        "summary": "Creatives lesen",
        "description": "Listet die Creatives des Kunden aus dem DB-Spiegel. Scope `reporting`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "ACTIVE",
                "PAUSED",
                "ARCHIVED"
              ]
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste der Creatives.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "link": {
                            "type": "string"
                          },
                          "headline": {
                            "type": "string"
                          },
                          "meta_creative_id": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "operationId": "createCreative",
        "tags": [
          "Creatives"
        ],
        "summary": "Medium als Creative-Entwurf registrieren",
        "description": "Registriert ein hochgeladenes Medium (image_hash aus /media/images ODER video_id aus /media/videos — genau eines) als Creative-ENTWURF. Es wird KEIN Meta-Creative gebaut und KEINE Copy gesetzt: Texte, CTA, Ziel-Link und angezeigter Link gehören zur Anzeige und werden in POST /ads übergeben — dort entsteht aus diesem Entwurf (creative_id) das finale Creative. Gleicher Ablauf wie beim Vorlagen-Render.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "image_hash": {
                    "type": "string",
                    "description": "Bild-Hash aus POST /media/images. Genau eines von image_hash oder video_id erforderlich.",
                    "example": "a1b2c3d4e5f67890a1b2c3d4e5f67890"
                  },
                  "video_id": {
                    "type": "string",
                    "description": "Video-ID aus POST /media/videos. Genau eines von image_hash oder video_id erforderlich. Muss spätestens bei POST /ads READY sein.",
                    "example": "239847562018"
                  },
                  "thumbnail_hash": {
                    "type": "string",
                    "description": "Nur bei video_id. Optionaler image_hash als Video-Thumbnail; ohne Angabe nutzt POST /ads das automatische Meta-Thumbnail."
                  },
                  "name": {
                    "type": "string",
                    "description": "Optionaler interner Name des Entwurfs.",
                    "example": "Headliner Visual"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creative-Entwurf angelegt.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "type": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/creatives/{id}": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Interne Creative-ID (aus POST/GET)."
        }
      ],
      "patch": {
        "operationId": "updateCreative",
        "tags": [
          "Creatives"
        ],
        "summary": "Creative umbenennen",
        "description": "Benennt ein Creative um. Inhalt (Bild, Text, Link) ist bei Meta unveränderlich — nur `name` lässt sich ändern.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "example": "Headliner Visual v2"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Creative aktualisiert.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "name": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "meta_creative_id": {
                          "type": "string"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      },
      "delete": {
        "operationId": "archiveCreative",
        "tags": [
          "Creatives"
        ],
        "summary": "Creative archivieren",
        "description": "Archiviert ein Creative (status=ARCHIVED) — nur in campaignKit; das Meta-Creative bleibt bestehen (wird ggf. von Ads weiterverwendet).\n",
        "responses": {
          "200": {
            "description": "Creative archiviert.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "status": {
                          "type": "string",
                          "example": "ARCHIVED"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/adsets": {
      "get": {
        "operationId": "listAdSets",
        "tags": [
          "AdSets"
        ],
        "summary": "Ad-Sets lesen",
        "description": "Listet die Ad-Sets des Kunden aus dem DB-Spiegel, optional nach Ziel (`target`) und Status gefiltert. Die Antwort enthält `target` statt einer Kampagnen-ID. Scope `reporting`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "target",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "ACTIVE",
                "PAUSED",
                "ARCHIVED"
              ]
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste der Ad-Sets.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "target": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "daily_budget_cents": {
                            "type": "integer"
                          },
                          "lifetime_budget_cents": {
                            "type": "integer"
                          },
                          "start_time": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "end_time": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "status": {
                            "type": "string"
                          },
                          "effective_status": {
                            "type": "string"
                          },
                          "meta_adset_id": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "operationId": "createAdSet",
        "tags": [
          "AdSets"
        ],
        "summary": "Ad-Set erstellen",
        "description": "Erstellt ein Ad-Set unter dem per `target` referenzierten Ziel. `optimization_goal`, `billing_event`, `targeting.publisher_platforms`, `targeting.facebook_positions`, `targeting.instagram_positions`, `targeting.device_platforms`, `targeting.geo_locations` sowie `promoted_object`, `name`, `status` und `bid_strategy` sind durch das Ziel vorgegeben — werden sie mitgeschickt, antwortet die API mit 422. Der Kunde liefert nur Budget/Schedule und optional Alter/Geschlecht/ Interessen. Budget ist **Pflicht**: genau eines von `daily_budget_cents` oder `lifetime_budget_cents` muss gesetzt sein — das Ziel führt kein Budget, daher liegt es verbindlich auf dem Ad-Set. `lifetime_budget_cents` erfordert zusätzlich `end_time`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target"
                ],
                "oneOf": [
                  {
                    "required": [
                      "daily_budget_cents"
                    ]
                  },
                  {
                    "required": [
                      "lifetime_budget_cents"
                    ]
                  }
                ],
                "properties": {
                  "target": {
                    "type": "string",
                    "description": "Ziel-Schlüssel des von uns eingerichteten Ziels. Bestimmt Optimierung, Platzierung, Geo, Pixel und DSA. Erhältst du von uns.\n",
                    "example": "sommerfest-2026"
                  },
                  "daily_budget_cents": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Tagesbudget in Cent (5000 = 50,00 €). Pflicht, sofern kein lifetime_budget_cents gesetzt ist (genau eines von beiden).\n",
                    "example": 5000
                  },
                  "lifetime_budget_cents": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Laufzeitbudget in Cent (50000 = 500,00 €). Pflicht, sofern kein daily_budget_cents gesetzt ist. Erfordert end_time.\n",
                    "example": 50000
                  },
                  "start_time": {
                    "type": "string",
                    "format": "date-time",
                    "example": "2026-06-01T10:00:00+02:00"
                  },
                  "end_time": {
                    "type": "string",
                    "format": "date-time",
                    "example": "2026-06-30T23:59:59+02:00"
                  },
                  "targeting": {
                    "type": "object",
                    "properties": {
                      "age_min": {
                        "type": "integer",
                        "minimum": 13,
                        "maximum": 65,
                        "example": 25
                      },
                      "age_max": {
                        "type": "integer",
                        "minimum": 13,
                        "maximum": 65,
                        "example": 45
                      },
                      "genders": {
                        "type": "array",
                        "items": {
                          "type": "integer",
                          "enum": [
                            1,
                            2
                          ]
                        },
                        "example": [
                          1,
                          2
                        ]
                      },
                      "flexible_spec": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "additionalProperties": true
                        },
                        "example": [
                          {
                            "interests": [
                              {
                                "id": "6003139266461",
                                "name": "Live music"
                              }
                            ]
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Ad-Set erstellt.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "meta_adset_id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/adsets/{id}": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Interne Ad-Set-ID (aus POST/GET)."
        }
      ],
      "patch": {
        "operationId": "updateAdSet",
        "tags": [
          "AdSets"
        ],
        "summary": "Ad-Set ändern",
        "description": "Aktualisiert Budget, Laufzeit oder Status eines Ad-Sets (partielles Update). Gesperrte Felder (optimization_goal, billing_event, targeting u. a.) führen zu 422. Erstes Aktivieren (`status: ACTIVE`) zählt gegen das Monatslimit des Plans und liefert bei Erschöpfung 402 `quota_exceeded`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "daily_budget_cents": {
                    "type": "integer",
                    "minimum": 1,
                    "example": 5000
                  },
                  "lifetime_budget_cents": {
                    "type": "integer",
                    "minimum": 1,
                    "example": 50000
                  },
                  "start_time": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "end_time": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "ACTIVE",
                      "PAUSED"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ad-Set aktualisiert.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "name": {
                          "type": "string"
                        },
                        "daily_budget_cents": {
                          "type": "integer"
                        },
                        "lifetime_budget_cents": {
                          "type": "integer"
                        },
                        "start_time": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "end_time": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "status": {
                          "type": "string"
                        },
                        "meta_adset_id": {
                          "type": "string"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/QuotaExceeded"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      },
      "delete": {
        "operationId": "archiveAdSet",
        "tags": [
          "AdSets"
        ],
        "summary": "Ad-Set archivieren",
        "description": "Archiviert ein Ad-Set (status=ARCHIVED) bei uns und bei Meta.",
        "responses": {
          "200": {
            "description": "Ad-Set archiviert.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "status": {
                          "type": "string",
                          "example": "ARCHIVED"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/ads": {
      "get": {
        "operationId": "listAds",
        "tags": [
          "Ads"
        ],
        "summary": "Ads lesen",
        "description": "Listet die Ads des Kunden inkl. gecachtem `effective_status` aus dem DB-Spiegel. Für den frischen Live-Status siehe `GET /status/{ad_id}`. Scope `reporting`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "adset_id",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "ACTIVE",
                "PAUSED",
                "ARCHIVED"
              ]
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste der Ads.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "ad_set_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "creative_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "effective_status": {
                            "type": "string"
                          },
                          "disapproval_reason": {
                            "type": "string",
                            "nullable": true
                          },
                          "meta_ad_id": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "operationId": "createAd",
        "tags": [
          "Ads"
        ],
        "summary": "Anzeige einstellen + erstellen",
        "description": "Stellt die Anzeige ein (wie im Werbeanzeigenmanager) und legt sie an. HIER gehört die Anzeigen-Copy hin (nicht ins Template): Primärer Text, Überschrift, Beschreibung (je genau ein Wert), Call-to-Action, Ziel-Link, optionaler angezeigter Link sowie Ad-Name und -Status. Aus dem Render-Entwurf (creative_id aus /v1/templates/render) wird das finale Multi-Ratio-Creative gebaut und die Ad erstellt. Ist creative_id bereits finalisiert (z. B. aus /v1/creatives), werden die Copy-Felder ignoriert.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ad_set_id",
                  "creative_id"
                ],
                "properties": {
                  "ad_set_id": {
                    "type": "string",
                    "format": "uuid",
                    "example": "a51f9c33-7e2b-4d18-8a0e-6b9c1d4f2e77"
                  },
                  "creative_id": {
                    "type": "string",
                    "format": "uuid",
                    "example": "1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d"
                  },
                  "link": {
                    "type": "string",
                    "format": "uri",
                    "description": "Echte Ziel-URL. PFLICHT, wenn creative_id ein Render-Entwurf ist. Nur die saubere URL angeben — UTMs hängt campaignKit automatisch an; KEINE eigenen Tracking-Parameter.\n",
                    "example": "https://acme-events.de/tickets"
                  },
                  "display_link": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "Optionaler angezeigter Link (Display-URL). Kurze, lesbare Marken-URL ohne https:// und ohne Query-Parameter.\n",
                    "example": "acme-events.de/tickets"
                  },
                  "call_to_action": {
                    "type": "string",
                    "default": "LEARN_MORE",
                    "enum": [
                      "SIGN_UP",
                      "SUBSCRIBE",
                      "SEE_MORE",
                      "DOWNLOAD",
                      "APPLY_NOW",
                      "BOOK_NOW",
                      "BUY_TICKETS",
                      "CONTACT_US",
                      "GET_OFFER",
                      "GET_QUOTE",
                      "GET_SHOWTIMES",
                      "WATCH_MORE",
                      "LEARN_MORE",
                      "LISTEN_NOW",
                      "ORDER_NOW"
                    ],
                    "example": "BUY_TICKETS"
                  },
                  "message": {
                    "type": "string",
                    "maxLength": 2200,
                    "description": "In Meta \"Primärer Text\": der wichtigste Text der Anzeige, erscheint in den meisten Platzierungen (z. B. Facebook-Feed) über oder unter Bild/Video. Vermittelt die Hauptbotschaft und macht neugierig. Maximal 125 Zeichen empfohlen; idealerweise nur 1 bis 3 Zeilen, damit der Text ohne \"Mehr anzeigen\" vollständig lesbar ist (die API akzeptiert technisch bis 2.200). Kein Call-to-Action. Pro Anzeige genau eine Variante (mehrere Text-Varianten sind mit den Multi-Format-Creatives nicht kombinierbar).\n",
                    "example": "Triff Gründer und Macher beim Networking-Event in Berlin am 18. Juli ab 19 Uhr."
                  },
                  "headline": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "In Meta \"Überschrift\": erscheint meist fett neben dem Call-to-Action-Button und hebt das wichtigste Verkaufsargument bzw. den Nutzen kurz hervor. Maximal 40 Zeichen empfohlen, dann bleibt sie auch auf kleinen Bildschirmen einzeilig (technisch bis 255). Kein Call-to-Action.\n",
                    "example": "Founders Night Berlin"
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "In Meta \"Beschreibung\": optionale Zusatzinfo unter der Überschrift, nicht in allen Platzierungen sichtbar. Für nicht essenzielle Details (z. B. Lieferinfo, kurzes Testimonial). Maximal 25 Zeichen empfohlen, da der Text oft abgeschnitten wird (technisch bis 255). Kein Call-to-Action.\n",
                    "example": "Fr. 18. Juli, 19 Uhr"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Optionaler Anzeigenname (sonst Naming Convention)."
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "PAUSED",
                      "ACTIVE"
                    ],
                    "default": "PAUSED",
                    "description": "PAUSED (Default) oder ACTIVE (nach Review ausspielen)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Ad erstellt.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "meta_ad_id": {
                          "type": "string"
                        },
                        "meta_creative_id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/ads/{id}": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Interne Ad-ID (aus POST/GET)."
        }
      ],
      "patch": {
        "operationId": "updateAd",
        "tags": [
          "Ads"
        ],
        "summary": "Ad ändern",
        "description": "Setzt den Status einer Ad (z. B. nach erfolgreichem Review aktivieren).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "ACTIVE",
                      "PAUSED"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ad aktualisiert.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "name": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "effective_status": {
                          "type": "string"
                        },
                        "meta_ad_id": {
                          "type": "string"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      },
      "delete": {
        "operationId": "archiveAd",
        "tags": [
          "Ads"
        ],
        "summary": "Ad archivieren",
        "description": "Archiviert eine Ad (status=ARCHIVED) bei uns und bei Meta.",
        "responses": {
          "200": {
            "description": "Ad archiviert.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "status": {
                          "type": "string",
                          "example": "ARCHIVED"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/targeting/geo": {
      "get": {
        "operationId": "targetingGeo",
        "tags": [
          "Targeting"
        ],
        "summary": "Stadt-Lookup (DACH City-Keys)",
        "description": "Sucht Städte über Metas Search-API und liefert die `key`s für `geo_locations.cities` im Ad-Set-Targeting. Scope `reporting`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "q",
            "required": true,
            "schema": {
              "type": "string",
              "example": "Berlin"
            }
          },
          {
            "in": "query",
            "name": "country_code",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "DE",
                "AT",
                "CH"
              ]
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Treffer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string",
                            "example": "2972535"
                          },
                          "name": {
                            "type": "string",
                            "example": "Berlin"
                          },
                          "type": {
                            "type": "string",
                            "example": "city"
                          },
                          "country_code": {
                            "type": "string",
                            "nullable": true,
                            "example": "DE"
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/targeting/interests": {
      "get": {
        "operationId": "targetingInterests",
        "tags": [
          "Targeting"
        ],
        "summary": "Interessen-Lookup",
        "description": "Sucht Interessen-Targeting-Optionen und liefert deren `id`s für `flexible_spec` im Ad-Set-Targeting. Scope `reporting`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "q",
            "required": true,
            "schema": {
              "type": "string",
              "example": "Live-Musik"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Treffer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "example": "6003139266461"
                          },
                          "name": {
                            "type": "string",
                            "example": "Live music"
                          },
                          "audience_size": {
                            "type": "integer",
                            "nullable": true,
                            "example": 248000000
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/status/{ad_id}": {
      "get": {
        "operationId": "getAdStatus",
        "tags": [
          "Status"
        ],
        "summary": "Live effective_status einer Ad",
        "description": "Holt effective_status + Ablehnungsgrund live aus der Meta-API, aktualisiert den serverseitigen Cache und liefert den frischen Wert. `ad_id` ist die interne UUID. Scope `reporting`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "ad_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Aktueller Status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "ad_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "effective_status": {
                          "type": "string",
                          "enum": [
                            "ACTIVE",
                            "PAUSED",
                            "PENDING_REVIEW",
                            "DISAPPROVED",
                            "ARCHIVED"
                          ]
                        },
                        "disapproval_reason": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/insights": {
      "get": {
        "operationId": "getInsights",
        "tags": [
          "Reporting"
        ],
        "summary": "Performance-Insights (tägliche Snapshots)",
        "description": "Liefert normalisierte Performance-Metriken (Spend, Impressionen, Reichweite, Klicks, CTR, CPC, CPM, Conversions, Conversion-Wert, ROAS, CPA) je Entity auf Ad-Set- oder Ad-Ebene und Zeitspanne. Die Daten stammen aus täglich gesyncten Snapshots (Meta ist Source of Truth); Conversions werden über ein 7-Tage-Fenster nachträglich aktualisiert. Geldwerte in Major-Units der Konto-Währung. Die Kampagnen-Ebene ist nicht verfügbar. level=ad-Antworten enthalten zusätzlich adset_id. Scope `reporting`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "level",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "adset",
                "ad"
              ]
            }
          },
          {
            "in": "query",
            "name": "entity_id",
            "required": false,
            "description": "Interne UUID eines Ad-Sets (level=adset) oder einer Ad (level=ad). Ohne Angabe werden alle Entities der Ebene zurückgegeben.\n",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "adset_id",
            "required": false,
            "description": "Nur mit level=ad: liefert die Metriken aller Ads dieses Ad-Sets — so erreichst du mit der Ad-Set-ID die enthaltenen Ads, ohne deren IDs zu kennen.\n",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "date_range",
            "required": false,
            "description": "Preset-String (today, yesterday, last_3d, last_7d, last_14d, last_28d, last_30d, last_90d, this_month, last_month, this_week_mon_today, last_week_mon_sun, maximum) oder JSON-Objekt {\"since\":\"YYYY-MM-DD\",\"until\":\"YYYY-MM-DD\"}. Default: last_30d.\n",
            "schema": {
              "type": "string",
              "example": "last_30d"
            }
          },
          {
            "in": "query",
            "name": "fields",
            "required": false,
            "description": "Kommagetrennte Auswahl der Kern-Metriken. Default ist das volle Set.",
            "schema": {
              "type": "string",
              "example": "spend,impressions,clicks,ctr,cpc,roas"
            }
          },
          {
            "in": "query",
            "name": "breakdowns",
            "required": false,
            "description": "Aufschlüsselung nach einer gesyncten Dimension: age, gender, country, publisher_platform, platform_position, impression_device, device_platform oder die Kombination \"age,gender\". Muss exakt einem gesyncten Set entsprechen.\n",
            "schema": {
              "type": "string",
              "example": "age,gender"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Normalisierte Metriken je Entity (bzw. je Entity und Breakdown-Wert).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "Interne UUID der Entity."
                          },
                          "adset_id": {
                            "type": "string",
                            "format": "uuid",
                            "nullable": true,
                            "description": "Nur bei level=ad: Eltern-Ad-Set (zum Gruppieren)."
                          },
                          "meta_id": {
                            "type": "string",
                            "nullable": true,
                            "description": "Meta-ID der Entity."
                          },
                          "breakdown": {
                            "type": "object",
                            "nullable": true,
                            "additionalProperties": {
                              "type": "string"
                            },
                            "example": {
                              "age": "25-34",
                              "gender": "female"
                            }
                          },
                          "spend": {
                            "type": "number",
                            "example": 1928
                          },
                          "impressions": {
                            "type": "integer",
                            "example": 187400
                          },
                          "reach": {
                            "type": "integer",
                            "example": 92100,
                            "description": "Erreichte Personen (Unique-Metrik). NICHT additiv über Tage: bei einem mehrtägigen Zeitraum die höchste Einzeltags-Reichweite (belastbare Untergrenze), bei einem einzelnen Tag exakt.\n"
                          },
                          "clicks": {
                            "type": "integer",
                            "example": 5082
                          },
                          "ctr": {
                            "type": "number",
                            "nullable": true,
                            "example": 2.71
                          },
                          "cpc": {
                            "type": "number",
                            "nullable": true,
                            "example": 0.38
                          },
                          "cpm": {
                            "type": "number",
                            "nullable": true,
                            "example": 10.29
                          },
                          "conversions": {
                            "type": "integer",
                            "example": 246
                          },
                          "conversion_value": {
                            "type": "number",
                            "example": 9832.8
                          },
                          "roas": {
                            "type": "number",
                            "nullable": true,
                            "example": 5.1
                          },
                          "cpa": {
                            "type": "number",
                            "nullable": true,
                            "example": 7.84
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/audiences": {
      "get": {
        "operationId": "listAudiences",
        "tags": [
          "Audiences"
        ],
        "summary": "Audiences lesen",
        "description": "Listet die Audiences des Kunden aus dem DB-Spiegel (Status/Größe aus dem letzten Sync, siehe last_synced_at). Scope `reporting`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "subtype",
            "schema": {
              "type": "string",
              "enum": [
                "CUSTOM",
                "LOOKALIKE"
              ]
            }
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "deleted"
              ]
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liste der Audiences.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Audience"
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/audiences/custom": {
      "post": {
        "operationId": "createCustomAudience",
        "tags": [
          "Audiences"
        ],
        "summary": "Custom Audience aus CRM-Kontakten",
        "description": "Erstellt eine Custom Audience aus rohen CRM-Kontakten. Kontakte werden serverseitig normalisiert und gehasht (SHA-256) und nicht gespeichert. Optionaler erster Batch (≤ 10.000) direkt beim Anlegen; weitere über POST /audiences/{id}/contacts. Custom Audiences setzen voraus, dass die Custom-Audience- Nutzungsbedingungen im Werbekonto akzeptiert sind UND das Werbekonto dafür freigeschaltet ist (sehr neue Konten oder Konten ohne ausgelieferte Anzeigen können noch geblockt sein). In beiden Fällen liefert Meta denselben Fehler; die Antwort ist 403 mit details.reason='custom_audience_unavailable'. Das ist KEIN API-Konfigurationsfehler: Nutzungsbedingungen akzeptieren und/oder zuerst eine Anzeige schalten und das Konto reifen lassen, dann erneut versuchen.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 255,
                    "example": "Käufer 2026"
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 1000
                  },
                  "customer_file_source": {
                    "type": "string",
                    "enum": [
                      "USER_PROVIDED_ONLY",
                      "PARTNER_PROVIDED_ONLY",
                      "BOTH_USER_AND_PARTNER_PROVIDED"
                    ],
                    "default": "USER_PROVIDED_ONLY"
                  },
                  "contacts": {
                    "type": "array",
                    "maxItems": 10000,
                    "items": {
                      "$ref": "#/components/schemas/AudienceContact"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Custom Audience erstellt.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "meta_audience_id": {
                          "type": "string"
                        },
                        "subtype": {
                          "type": "string",
                          "example": "CUSTOM"
                        },
                        "contacts_received": {
                          "type": "integer"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/audiences/lookalike": {
      "post": {
        "operationId": "createLookalikeAudience",
        "tags": [
          "Audiences"
        ],
        "summary": "Lookalike Audience erstellen",
        "description": "Erzeugt eine Lookalike-Audience aus einer eigenen Custom Audience (per interner UUID) für ein Land mit Ähnlichkeitsgrad (empfohlen 1–10 %). Kann wie Custom Audiences mit 403 (details.reason='custom_audience_unavailable') ablehnen, wenn das Werbekonto noch nicht für Custom Audiences freigeschaltet ist oder die Nutzungsbedingungen fehlen. Das ist KEIN API-Konfigurationsfehler.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "source_audience_id",
                  "country",
                  "ratio"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 255,
                    "example": "LAL Käufer 2026 DE 3%"
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 1000
                  },
                  "source_audience_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "country": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 2,
                    "example": "DE"
                  },
                  "ratio": {
                    "type": "number",
                    "minimum": 0.01,
                    "maximum": 0.2,
                    "example": 0.03
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Lookalike Audience erstellt.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "meta_audience_id": {
                          "type": "string"
                        },
                        "subtype": {
                          "type": "string",
                          "example": "LOOKALIKE"
                        },
                        "source_audience_id": {
                          "type": "string",
                          "format": "uuid"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/audiences/{id}": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Interne Audience-ID."
        }
      ],
      "get": {
        "operationId": "getAudience",
        "tags": [
          "Audiences"
        ],
        "summary": "Audience-Detail inkl. Live-Status",
        "description": "Liefert eine Audience inkl. frischem Status von Meta (operation_status, delivery_status, approximate_count) und `ready` (true = in Ad-Sets nutzbar). Scope `reporting`.\n",
        "responses": {
          "200": {
            "description": "Audience-Detail.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AudienceDetail"
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "updateAudience",
        "tags": [
          "Audiences"
        ],
        "summary": "Audience ändern",
        "description": "Ändert Name und/oder Beschreibung einer Audience.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 255
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 1000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Audience aktualisiert.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "name": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true
                        },
                        "subtype": {
                          "type": "string"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      },
      "delete": {
        "operationId": "deleteAudience",
        "tags": [
          "Audiences"
        ],
        "summary": "Audience löschen",
        "description": "Löscht eine Audience bei Meta und markiert den Spiegel als deleted.",
        "responses": {
          "200": {
            "description": "Audience gelöscht.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "status": {
                          "type": "string",
                          "example": "deleted"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/audiences/{id}/contacts": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Interne Audience-ID (nur CUSTOM)."
        }
      ],
      "post": {
        "operationId": "addAudienceContacts",
        "tags": [
          "Audiences"
        ],
        "summary": "Kontakte hinzufügen",
        "description": "Fügt einer Custom Audience Kontakte hinzu (append, ≤ 10.000). Rohe Kontakte im festen Schema, serverseitig gehasht.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "contacts"
                ],
                "properties": {
                  "contacts": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 10000,
                    "items": {
                      "$ref": "#/components/schemas/AudienceContact"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kontakte hinzugefügt.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "contacts_received": {
                          "type": "integer"
                        },
                        "contacts_invalid": {
                          "type": "integer"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      },
      "delete": {
        "operationId": "removeAudienceContacts",
        "tags": [
          "Audiences"
        ],
        "summary": "Kontakte entfernen",
        "description": "Entfernt Kontakte aus einer Custom Audience (DSGVO-Opt-out). Gleiches Kontakt-Schema wie beim Hinzufügen.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "contacts"
                ],
                "properties": {
                  "contacts": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 10000,
                    "items": {
                      "$ref": "#/components/schemas/AudienceContact"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kontakte entfernt.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "contacts_removed": {
                          "type": "integer"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "tags": [
          "Webhooks"
        ],
        "summary": "Registrierte Webhooks lesen",
        "description": "Listet die Webhook-Endpoints des Kunden (ohne Signatur-Secret). Scope `reporting`.\n",
        "responses": {
          "200": {
            "description": "Liste der Endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "events": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "inactive"
                            ]
                          },
                          "last_delivery_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "failure_count": {
                            "type": "integer"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "operationId": "registerWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Webhook-URL registrieren",
        "description": "Registriert eine HTTPS-URL für Status-Push (ad.status_changed). Die Antwort enthält EINMALIG ein `signing_secret`; jede Zustellung wird damit per HMAC-SHA256 signiert (Header `X-CampaignKit-Signature: t=<unix>,v1=<hmac>`, gebildet über `\"<t>.<roher Body>\"`). Scope `full`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "HTTPS-Endpunkt der Kundenplattform.",
                    "example": "https://kunde.de/hooks/campaignkit"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "default": [
                      "ad.status_changed"
                    ],
                    "example": [
                      "ad.status_changed"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook registriert.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "events": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "active": {
                          "type": "boolean"
                        },
                        "signing_secret": {
                          "type": "string",
                          "description": "Nur einmalig — danach nicht mehr abrufbar."
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/plan": {
      "get": {
        "operationId": "getPlan",
        "tags": [
          "Plan"
        ],
        "summary": "Vertragszustand + Stufe + voraussichtliche Gebühr",
        "description": "Liefert den Vertragszustand, ob Schreibzugriffe möglich sind, das laufende Zyklusfenster (am Vertrags-Starttag verankert), den bisherigen Spend der verwalteten Kampagnen, die voraussichtliche Gebühr (min über Stufen) und die Tarif-Staffel. Keine Tarifwahl: die Stufe ergibt sich rückwirkend aus dem Spend; solange aktiv gilt mindestens die Starter-Stufe. Beträge in Cent, netto. Scope `reporting`.\n",
        "responses": {
          "200": {
            "description": "Vertragszustand + Projektion.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "account_status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "deactivated",
                            "past_due",
                            "pending",
                            "canceled"
                          ]
                        },
                        "can_write": {
                          "type": "boolean"
                        },
                        "current_cycle": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "start": {
                              "type": "string",
                              "format": "date"
                            },
                            "end": {
                              "type": "string",
                              "format": "date"
                            }
                          }
                        },
                        "spend_to_date_cents": {
                          "type": "integer"
                        },
                        "projected": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "tier": {
                              "type": "string"
                            },
                            "fix_cents": {
                              "type": "integer"
                            },
                            "media_fee_cents": {
                              "type": "integer"
                            },
                            "total_cents": {
                              "type": "integer"
                            }
                          }
                        },
                        "last_billed": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "tier": {
                              "type": "string",
                              "nullable": true
                            },
                            "total_cents": {
                              "type": "integer"
                            }
                          }
                        },
                        "tiers": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "key": {
                                "type": "string"
                              },
                              "name": {
                                "type": "string"
                              },
                              "fix_cents": {
                                "type": "integer"
                              },
                              "media_fee_pct": {
                                "type": "number"
                              },
                              "spend_min_cents": {
                                "type": "integer"
                              },
                              "spend_max_cents": {
                                "type": "integer",
                                "nullable": true
                              }
                            }
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/plan/usage": {
      "get": {
        "operationId": "getPlanUsage",
        "tags": [
          "Plan"
        ],
        "summary": "Laufender Zyklus + Spend + Projektion",
        "description": "Laufendes Zyklusfenster, bisher angefallener Spend der über campaignKit verwalteten Kampagnen und die voraussichtliche Gebühr. KEIN Limit — der Spend wird nie gedeckelt. Scope `reporting`.\n",
        "responses": {
          "200": {
            "description": "Zyklus + Spend + Projektion.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "cycle_start": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "cycle_end": {
                          "type": "string",
                          "format": "date",
                          "nullable": true
                        },
                        "spend_to_date_cents": {
                          "type": "integer"
                        },
                        "projected_tier": {
                          "type": "string",
                          "nullable": true
                        },
                        "projected_fee_cents": {
                          "type": "integer",
                          "nullable": true
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/account/deactivate": {
      "post": {
        "operationId": "deactivateAccount",
        "tags": [
          "Plan"
        ],
        "summary": "Konto deaktivieren (0 €, Selbst-Service)",
        "description": "Deaktiviert das Konto: Schreibzugriffe (neue oder aktive Ads) werden gesperrt, Lesen bleibt möglich. Der laufende Zyklus wird am Zyklusende noch abgerechnet, danach 0 € bis zur Reaktivierung. Die Reaktivierung erfolgt NICHT per API, sondern auf Anfrage beim Anbieter per E-Mail an kontakt@ventureon.io. Dabei kann eine Reaktivierungsgebühr anfallen (standardmäßig 179 € netto zzgl. USt; die Höhe richtet sich nach dem zum Zeitpunkt der Reaktivierung gültigen Tarif). Idempotent. Scope `reporting` (auch im bereits deaktivierten Zustand aufrufbar).\n",
        "responses": {
          "200": {
            "description": "Konto deaktiviert.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "account_status": {
                          "type": "string",
                          "example": "deactivated"
                        },
                        "already_deactivated": {
                          "type": "boolean"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Kunden-API-Key als `Authorization: Bearer <key>`. Pro Kunde rotierbar, mit Scope (`full` oder `reporting`).\n"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Kein oder ungültiger API-Key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "API-Key gültig, aber Scope reicht nicht.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "Kein aktiver Schreibzugang (Konto deaktiviert, Zahlung überfällig, Checkout offen oder Vertrag beendet). Lesen bleibt im deaktivierten/überfälligen Zustand möglich.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate-Limit überschritten.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ValidationError": {
        "description": "Ungültige Eingabe oder gesperrtes Feld gesetzt. Auch bei unvollständigem Onboarding des Kunden (fehlende Pflicht-Stammdaten wie Page, Instagram-Konto, DSA/legal_name oder unverifizierter Token); `details.missing` listet die fehlenden Schlüssel.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Ressource oder Endpunkt nicht gefunden (falsche ID oder unbekannter Pfad).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "HTTP-Methode für diesen Endpunkt nicht erlaubt.",
        "headers": {
          "Allow": {
            "description": "Kommagetrennte Liste der erlaubten Methoden (RFC 9110).",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "QuotaExceeded": {
        "description": "Plan-Limit (aktive Ad-Sets/Monat) erreicht — Upgrade nötig.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "Statuskollision (z. B. deaktivierte Ziel-Kampagne beim Anlegen/Ändern/Aktivieren von Ad-Sets oder Ads) oder Idempotenz-Konflikt.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "PublicTemplateSummary": {
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "type"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "type": {
            "type": "string",
            "enum": [
              "image",
              "video"
            ],
            "description": "Medium der Vorlage. \"video\", sobald sie einen Video-Layer hat, sonst \"image\"."
          }
        }
      },
      "PublicTemplateParam": {
        "type": "object",
        "required": [
          "name",
          "type",
          "required",
          "label",
          "description",
          "min_chars",
          "max_chars",
          "example"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "url",
              "color",
              "string"
            ]
          },
          "required": {
            "type": "boolean"
          },
          "label": {
            "type": "string",
            "nullable": true,
            "description": "Kurzer Anzeigename."
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Hinweis, was inhaltlich in dieses Feld gehört (für KI-Inhalte)."
          },
          "min_chars": {
            "type": "integer",
            "nullable": true
          },
          "max_chars": {
            "type": "integer",
            "nullable": true
          },
          "example": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "PublicTemplateDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PublicTemplateSummary"
          },
          {
            "type": "object",
            "required": [
              "params"
            ],
            "properties": {
              "params": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PublicTemplateParam"
                }
              }
            }
          }
        ]
      },
      "TemplatePreviewUrls": {
        "type": "object",
        "description": "URL des Vorschau-Bild-Endpunkts je Format (1:1, 4:5, 9:16).",
        "properties": {
          "1:1": {
            "type": "string",
            "format": "uri"
          },
          "4:5": {
            "type": "string",
            "format": "uri"
          },
          "9:16": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "TemplatePreviewParams": {
        "type": "object",
        "required": [
          "template",
          "params",
          "preview_urls"
        ],
        "properties": {
          "template": {
            "type": "string",
            "format": "uuid"
          },
          "params": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Gespeicherte eigene Parameter-Werte (leer, wenn nichts gesetzt)."
          },
          "preview_urls": {
            "$ref": "#/components/schemas/TemplatePreviewUrls"
          }
        }
      },
      "Audience": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "meta_audience_id": {
            "type": "string",
            "nullable": true
          },
          "subtype": {
            "type": "string",
            "enum": [
              "CUSTOM",
              "LOOKALIKE"
            ]
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "customer_file_source": {
            "type": "string",
            "nullable": true
          },
          "approximate_count": {
            "type": "integer",
            "nullable": true
          },
          "operation_status_code": {
            "type": "integer",
            "nullable": true
          },
          "delivery_status_code": {
            "type": "integer",
            "nullable": true
          },
          "ready": {
            "type": "boolean"
          },
          "origin_audience_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "country": {
            "type": "string",
            "nullable": true
          },
          "ratio": {
            "type": "number",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "deleted"
            ]
          },
          "last_synced_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AudienceDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Audience"
          },
          {
            "type": "object",
            "properties": {
              "operation_status": {
                "type": "string",
                "nullable": true,
                "description": "Klartext-Beschreibung des Operation-Status."
              },
              "delivery_status": {
                "type": "string",
                "nullable": true,
                "description": "Klartext-Beschreibung des Delivery-Status."
              }
            }
          }
        ]
      },
      "AudienceContact": {
        "type": "object",
        "additionalProperties": false,
        "description": "Roher CRM-Kontakt im festen Schema. Mindestens eines von email, phone oder external_id. Serverseitig normalisiert und gehasht (SHA-256); external_id (EXTERN_ID) wird nicht gehasht.\n",
        "properties": {
          "email": {
            "type": "string",
            "example": "max@example.com"
          },
          "phone": {
            "type": "string",
            "example": "+4915112345678"
          },
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "zip": {
            "type": "string",
            "example": "50667"
          },
          "country": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2,
            "example": "DE"
          },
          "year_of_birth": {
            "type": "integer",
            "minimum": 1900,
            "maximum": 2100
          },
          "gender": {
            "type": "string",
            "enum": [
              "m",
              "f"
            ]
          },
          "external_id": {
            "type": "string"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "request_id"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "payment_required",
                  "quota_exceeded",
                  "forbidden",
                  "not_found",
                  "validation_error",
                  "conflict",
                  "rate_limited",
                  "method_not_allowed",
                  "internal_error",
                  "not_implemented"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {
                "description": "Optionale, strukturierte Zusatzinfos."
              }
            }
          },
          "request_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      }
    }
  }
}