# CampaignKit API > Middleware vor der Meta Marketing API. Einheitliche, versionierte Requests; > der API-Key bestimmt den Meta-Account, der Meta-Token verlässt nie den Server. Base URL: https://api.campaignkit.ventureon.io/v1 Docs: https://docs.campaignkit.ventureon.io Auth: Authorization: Bearer Version: v1 (gepinnt auf Meta v25.0) ## Standard-Header - Authorization (immer): Bearer . Bestimmt Kunde + Plan. Niemals clientseitig im Browser verwenden. - Content-Type (bei JSON-Body): application/json — entfällt bei Multipart-Uploads (Media). - Idempotency-Key (empfohlen (POST)): Eindeutige UUID pro Erstellung. Wiederholte Requests mit gleichem Key erzeugen keine Dubletten. - CampaignKit-Version (optional): Pinnt die API-Version (z. B. v1). Ohne Header gilt die Account-Default-Version. - X-Request-Id (optional): Eigene Korrelations-ID; wird in Logs & Support-Anfragen zurückgegeben. ## Fehlercodes - 400 bad_request: Request-Format ungültig (JSON-Syntax, fehlende Felder). - 401 unauthorized: API-Key fehlt, ungültig oder rotiert. - 403 forbidden: Key-Scope deckt diese Operation nicht (z. B. reporting-only). - 404 not_found: Ressource oder Endpunkt existiert nicht (falsche ID oder unbekannter Pfad). - 405 method_not_allowed: HTTP-Methode für diesen Endpunkt nicht erlaubt (erlaubte Methoden im Allow-Header). - 409 conflict: Idempotenz-Konflikt oder Statuskollision (u. a. deaktivierte Ziel-Kampagne beim Anlegen/Ändern/Aktivieren von Ad-Sets oder Ads). - 402 payment_required: Kein aktiver Schreibzugang: Konto deaktiviert, Zahlung überfällig, Checkout offen oder Vertrag beendet (Lesen bleibt im deaktivierten/überfälligen Zustand möglich). - 422 validation_error: Felder vorhanden, aber inhaltlich ungültig (siehe details[]); auch bei unvollständigem Onboarding des Kunden (details.missing listet fehlende Pflichtdaten). - 429 rate_limited: Zu viele Requests → Retry-After beachten. - 5xx internal_error: Fehler bei uns oder bei Meta. Mit Backoff erneut versuchen. ## Endpoints ### System Verbindung & Health. Der einzige bereits implementierte Endpunkt — prüft API-Key und Erreichbarkeit. #### GET /v1/health Prüft, dass der API-Key gültig und die API erreichbar ist. Erfordert mindestens den Scope reporting. Ideal für Status-Monitoring und um die Anbindung beim Onboarding zu verifizieren. Response: ```json { "data": { "status": "ok", "api_version": "v1" }, "request_id": "550e8400-e29b-41d4-a716-446655440000" } ``` ### Media Übergibt fertige Bild und Video-Assets per URL an Meta und liefert die Referenz (image_hash bzw. video_id) für Creatives. Wir komprimieren NICHT und speichern die Datei nicht. Der Kunde ist für Format, Größe, Kompression und Versionierung selbst zuständig und übergibt nur die finale, öffentlich erreichbare URL. #### POST /v1/media/images Übergibt ein Bild per öffentlich erreichbarer URL. Unser Server lädt die Datei von der URL und reicht die Bytes UNVERÄNDERT (keine Kompression) an Meta weiter; zurück kommt ein image_hash für Creatives. Synchron. Die URL wird serverseitig validiert (nur https, keine internen Ziele, keine Weiterleitungen). Body-Parameter: - url (string (https URL), erforderlich): Direkte, öffentlich erreichbare https-URL zur fertigen Bilddatei (JPG/PNG/WebP, ≤ 30 MB). Wenn ihr das von uns aufgesetzte Supabase nutzt: signierte URL via supabase.storage.from(bucket).createSignedUrl(path, 3600) erzeugen. Ohne Supabase: jede öffentlich erreichbare https-URL (CDN, S3 Presigned-URL o. ä.). Sie muss von unserem Backend abrufbar sein und darf NICHT weiterleiten. (z. B. https://.supabase.co/storage/v1/object/sign/media/headliner.jpg?token=…) Request: ``` curl -X POST https://api.campaignkit.ventureon.io/v1/media/images \ -H "Authorization: Bearer ak_live_..." \ -H "Content-Type: application/json" \ -d '{"url":"https://cdn.example.com/headliner.jpg"}' ``` Response: ```json { "data": { "id": "b3f1…", "image_hash": "a1b2c3d4e5f6", "url": "https://…fbcdn.net/…" }, "request_id": "…" } ``` Hinweis: Richtlinien: JPG/PNG/WebP, ≤ 30 MB, empfohlen ≥ 1080 px Kantenlänge. Seitenverhältnis je Platzierung (1:1, 4:5, 9:16, 1.91:1). Komprimierung macht der Kunde. #### GET /v1/media/images Listet die Ad-Images des Werbekontos direkt aus Meta (hash, Vorschau-URL, Maße, Name, Erstellzeit). Da Konto und Kunde 1:1 zugeordnet sind, ist das die komplette Bild-Mediathek des Kunden. Optional per hash auf ein einzelnes Bild gefiltert. Query-Parameter: - hash (string, optional): Auf einen einzelnen image_hash filtern. (z. B. a1b2c3d4e5f6) Request: ``` curl https://api.campaignkit.ventureon.io/v1/media/images \ -H "Authorization: Bearer ak_live_..." ``` Response: ```json { "data": [ { "image_hash": "a1b2c3d4e5f6", "url": "https://…fbcdn.net/…", "width": 1080, "height": 1080, "name": "headliner.jpg", "created_time": "2026-06-15T12:00:00+0000" } ], "request_id": "…" } ``` Hinweis: Reporting-Scope genügt. Quelle ist Meta live (zeigt auch extern angelegte Bilder). #### POST /v1/media/videos Übergibt ein Video per öffentlich erreichbarer URL. Anders als beim Bild lädt META die Datei selbst von der URL (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. Die Verarbeitung bei Meta ist asynchron: über GET /v1/media/videos/{id} pollen, bis READY, bevor das Video in einem Creative genutzt wird. Body-Parameter: - url (string (https URL), erforderlich): Direkte, öffentlich erreichbare https-URL zum fertigen Video (MP4/MOV). Wenn ihr das von uns aufgesetzte Supabase nutzt: signierte URL via supabase.storage.from(bucket).createSignedUrl(path, 3600) erzeugen (Gültigkeit ≥ 1 h, da Meta sie abruft). Ohne Supabase: jede öffentlich erreichbare https-URL (CDN, S3 Presigned-URL o. ä.). Sie muss von Metas Servern abrufbar sein. (z. B. https://.supabase.co/storage/v1/object/sign/media/teaser.mp4?token=…) Request: ``` curl -X POST https://api.campaignkit.ventureon.io/v1/media/videos \ -H "Authorization: Bearer ak_live_..." \ -H "Content-Type: application/json" \ -d '{"url":"https://cdn.example.com/teaser.mp4"}' ``` Response: ```json { "data": { "id": "b3f1…", "video_id": "239847562018", "status": "PROCESSING" }, "request_id": "…" } ``` Hinweis: Richtlinien: MP4/MOV, H.264 + AAC, ≤ 4 GB. Seitenverhältnis 1:1, 4:5, 9:16 oder 16:9. Async: über GET /v1/media/videos/{id} pollen, bis status=READY. #### GET /v1/media/videos 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 komplette Video-Mediathek des Kunden. Request: ``` curl https://api.campaignkit.ventureon.io/v1/media/videos \ -H "Authorization: Bearer ak_live_..." ``` Response: ```json { "data": [ { "video_id": "239847562018", "title": "Teaser", "status": "READY", "length": 15, "created_time": "2026-06-15T12:00:00+0000" } ], "request_id": "…" } ``` Hinweis: Reporting-Scope genügt. Quelle ist Meta live (zeigt auch extern angelegte Videos). #### GET /v1/media/videos/{id} Liefert den aktuellen Verarbeitungsstatus eines Videos: PROCESSING (Meta transkodiert noch), READY (nutzbar im Creative) oder FAILED (Verarbeitung fehlgeschlagen). {id} ist die interne ID aus POST /v1/media/videos. Path-Parameter: - id (string (UUID), erforderlich): Interne Video-Asset-ID aus POST /v1/media/videos. Request: ``` curl https://api.campaignkit.ventureon.io/v1/media/videos/b3f1… \ -H "Authorization: Bearer ak_live_..." ``` Response: ```json { "data": { "id": "b3f1…", "video_id": "239847562018", "status": "READY", "processed_at": "2026-06-15T12:00:00Z" }, "request_id": "…" } ``` Hinweis: Reporting-Scope genügt. PROCESSING heißt: erneut pollen. ### Ad Sets Oberste für dich sichtbare Ebene (Ad Set → Ad). Beim Anlegen referenzierst du über `target` (Ziel-Schlüssel) das von uns eingerichtete Ziel — die übergeordnete Kampagne und ihre Konfiguration verwalten wir für dich. Du lieferst nur Budget/Laufzeit und optional Alter/Geschlecht/Interessen — Optimierung, Platzierung, Geo, Abrechnung, DSA und der Name kommen verbindlich aus der hinterlegten Konfiguration. #### POST /v1/adsets Legt ein Ad Set unter dem per `target` referenzierten Ziel an. Budget ist Pflicht (genau eines von daily_budget_cents oder lifetime_budget_cents) — das Ziel führt kein Budget, daher liegt es verbindlich auf dem Ad-Set. Der Name wird automatisch vergeben (Naming Convention). Optimierungsziel, Abrechnung, Platzierung (Plattformen/Positionen), Geo und promoted_object sind durch das Ziel gesperrt und werden serverseitig gesetzt. Body-Parameter: - target (string, erforderlich): Ziel-Schlüssel des von uns eingerichteten Ziels. Bestimmt Optimierung, Platzierung, Geo, Pixel und DSA. Den Schlüssel erhältst du von uns. (z. B. sommerfest-2026) - daily_budget_cents (integer (Cent), erforderlich): Tagesbudget in Cent (5000 = 50,00 € in Konto-Währung). Pflicht: genau eines von daily_budget_cents ODER lifetime_budget_cents muss gesetzt sein — das Ziel setzt kein Budget, daher liegt das Budget verbindlich auf dem Ad-Set. (z. B. 5000) - lifetime_budget_cents (integer (Cent), erforderlich): Laufzeitbudget in Cent (50000 = 500,00 €). Pflicht: genau eines von daily_budget_cents ODER lifetime_budget_cents. Bei lifetime_budget_cents ist end_time zwingend erforderlich. (z. B. 50000) - start_time (string (ISO 8601), optional): Startzeitpunkt. Default: sofort. (z. B. 2026-06-01T10:00:00+02:00) - end_time (string (ISO 8601), optional): Endzeitpunkt. Bei lifetime_budget_cents Pflicht. (z. B. 2026-06-30T23:59:59+02:00) - targeting (object, optional): Optionale Zusatz-Zielgruppe. Nur die folgenden Unterfelder sind erlaubt — alles andere ist durch das Ziel gesperrt. (z. B. {"age_min":25,"age_max":45,"genders":[1,2]}) - targeting.age_min (integer, optional): Mindestalter. 13–65. (z. B. 25) - targeting.age_max (integer, optional): Höchstalter. 13–65 (65 = '65+'). (z. B. 45) - targeting.genders (integer[], optional): 1 = Männlich, 2 = Weiblich. Weggelassen = alle. (z. B. [1,2]) - targeting.flexible_spec (object[], optional): Interessen-/Verhaltens-Targeting. Array von OR-Gruppen (untereinander AND-verknüpft), je mit interests/behaviors/life_events als [{id,name}]. IDs kommen aus dem Interessen-Lookup. (z. B. [{"interests":[{"id":"6003139266461","name":"Live music"}]}]) Request: ``` POST /v1/adsets { "target": "sommerfest-2026", "daily_budget_cents": 5000, "targeting": { "age_min": 25, "age_max": 45, "flexible_spec": [{ "interests": [{ "id": "6003139266461", "name": "Live music" }] }] } } ``` Response: ```json { "data": { "id": "a51f…", "meta_adset_id": "604839271650", "name": "[CK] … · AdSet #1" }, "request_id": "…" } ``` Hinweis: Gesperrt (→ 422 validation_error mit details.locked_fields, wenn gesendet): name, status, optimization_goal, billing_event, bid_strategy, promoted_object sowie targeting.geo_locations / publisher_platforms / facebook_positions / instagram_positions / device_platforms. Das Anlegen ist nur unter einer AKTIVEN Ziel-Kampagne möglich; ist die Kampagne deaktiviert, kommt 409 conflict. Erfordert zudem vollständiges Onboarding (Token verifiziert, Werbekonto, Page, Instagram-Konto, DSA/legal_name); fehlt etwas → 422 mit details.missing. #### GET /v1/adsets Listet Ad Sets, optional nach Ziel (target) und Status gefiltert. Die Antwort enthält das Ziel (target) statt einer Kampagnen-ID. Query-Parameter: - target (string, optional): Auf ein Ziel (Ziel-Schlüssel) filtern. - status (enum, optional): ACTIVE | PAUSED | ARCHIVED. - limit (integer, optional): Anzahl pro Seite (Default 25, max 100). - offset (integer, optional): Versatz für Pagination (Default 0). Response: ```json { "data": [ { "id": "a51f9c33-7e2b-4d18-8a0e-6b9c1d4f2e77", "target": "sommerfest-2026", "name": "[CK] … · AdSet #1", "daily_budget_cents": 5000, "status": "ACTIVE", "effective_status": "ACTIVE", "meta_adset_id": "604839271650" } ], "request_id": "…" } ``` #### PATCH /v1/adsets/{id} Aktualisiert Budget, Laufzeit oder Status eines Ad Sets. Partielles Update — nur gesendete Felder ändern sich. Path-Parameter: - id (string (UUID), erforderlich): Interne Ad-Set-ID (aus POST/GET). Body-Parameter: - daily_budget_cents (integer (Cent), optional): Neues Tagesbudget. - lifetime_budget_cents (integer (Cent), optional): Neues Laufzeitbudget. - start_time (string (ISO 8601), optional): Neuer Startzeitpunkt. - end_time (string (ISO 8601), optional): Neuer Endzeitpunkt. - status (enum, optional): ACTIVE | PAUSED. Kein Mengen-Limit mehr; Aktivieren setzt nur einen aktiven Zugang + eine aktive Ziel-Kampagne voraus. Response: ```json { "data": { "id": "a51f…", "status": "ACTIVE", "daily_budget_cents": 5000, "meta_adset_id": "604839271650" }, "request_id": "…" } ``` Hinweis: Gesperrt (→ 422): optimization_goal, billing_event, targeting u. a. (siehe POST). Kein Mengen-Limit. Jede Änderung (auch Pausieren) erfordert eine aktive Ziel-Kampagne, sonst 409 conflict. Archivieren (DELETE) bleibt möglich. #### DELETE /v1/adsets/{id} Archiviert ein Ad Set (status=ARCHIVED) bei uns und bei Meta. Path-Parameter: - id (string (UUID), erforderlich): Interne Ad-Set-ID. Response: ```json { "data": { "id": "a51f…", "status": "ARCHIVED" }, "request_id": "…" } ``` Hinweis: Archiviert statt hart zu löschen. ### Creatives Visuelles Asset (Bild ODER Video) + Text, Link und Call-to-Action. Die Absender-Identität (Facebook-Page, optional Instagram) kommt aus der Kunden-Konfiguration — nicht aus dem Request. #### POST /v1/creatives Registriert ein zuvor hochgeladenes Medium (image_hash aus /v1/media/images ODER video_id aus /v1/media/videos) als Creative-ENTWURF. Genau eines von beiden angeben. Hier wird KEIN Meta-Creative gebaut und KEINE Copy gesetzt: Texte, CTA, Ziel-Link und angezeigter Link gehören zur Anzeige und werden in POST /v1/ads übergeben — dort entsteht aus diesem Entwurf (creative_id) das finale Creative. Gleicher Ablauf wie beim Vorlagen-Render. Body-Parameter: - image_hash (string, optional): Bild-Hash aus POST /v1/media/images. Genau eines von image_hash oder video_id ist erforderlich. (z. B. a1b2c3d4e5f67890a1b2c3d4e5f67890) - video_id (string, optional): Video-ID aus POST /v1/media/videos. Genau eines von image_hash oder video_id ist erforderlich. Das Video muss spätestens beim Anlegen der Anzeige (POST /v1/ads) READY sein. (z. B. 239847562018) - thumbnail_hash (string, optional): Nur bei video_id. Optionaler image_hash als Video-Thumbnail. Ohne Angabe wird in POST /v1/ads das von Meta automatisch generierte Thumbnail verwendet (Video muss dann READY sein). (z. B. f0e1d2c3b4a5) - name (string, optional): Optionaler interner Name des Entwurfs. (z. B. Headliner Visual) Request: ``` # Bild-Entwurf POST /v1/creatives { "image_hash": "a1b2c3d4e5f6" } # Video-Entwurf POST /v1/creatives { "video_id": "239847562018" } ``` Response: ```json { "data": { "id": "1f0c…", "type": "IMAGE", "status": "DRAFT" }, "request_id": "…" } ``` Hinweis: Liefert die `id` (= creative_id für POST /v1/ads) — noch KEINE meta_creative_id. Texte, CTA, Ziel-Link und Display-Link setzt du in POST /v1/ads; dort wird das finale Meta-Creative gebaut. Onboarding (Token, Werbekonto, Page, Instagram, DSA) wird beim Anlegen der Anzeige geprüft. #### GET /v1/creatives Listet die Creatives des Kontos, optional nach Status gefiltert. Query-Parameter: - status (enum, optional): ACTIVE | PAUSED | ARCHIVED. - limit (integer, optional): Anzahl pro Seite (Default 25, max 100). - offset (integer, optional): Versatz für Pagination (Default 0). Response: ```json { "data": [ { "id": "1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d", "name": "Headliner Visual", "type": "IMAGE", "status": "ACTIVE", "meta_creative_id": "120210394857" } ], "request_id": "…" } ``` #### PATCH /v1/creatives/{id} Benennt ein Creative um. Inhalt (Bild, Text, Link) ist bei Meta unveränderlich — nur der Name lässt sich ändern. Path-Parameter: - id (string (UUID), erforderlich): Interne Creative-ID (aus POST/GET). Body-Parameter: - name (string, erforderlich): Neuer interner Name (1–200 Zeichen). Response: ```json { "data": { "id": "1f0c…", "name": "Headliner Visual v2", "type": "IMAGE", "meta_creative_id": "120210394857" }, "request_id": "…" } ``` #### DELETE /v1/creatives/{id} Archiviert ein Creative (status=ARCHIVED). Path-Parameter: - id (string (UUID), erforderlich): Interne Creative-ID. Response: ```json { "data": { "id": "1f0c…", "status": "ARCHIVED" }, "request_id": "…" } ``` Hinweis: Archiviert statt hart zu löschen — nur in campaignKit, das Meta-Creative bleibt bestehen (wird ggf. von Ads weiterverwendet). ### Templates Zwei Wege: (1) VORLAGEN ANZEIGEN — GET /v1/templates liefert je Vorlage bereits fertige, gecachte `preview_urls` (je Format), die du unverändert direkt als Bild einbettest (img-Tag). Kein eigenes Rendern nötig, kein API-Key in der Bild-URL. (2) EIGENES CREATIVE — GET /v1/templates/{id} (Felder lesen) + POST /v1/templates/render (eigene Inhalte) erzeugt ein Werbemittel für POST /v1/ads. #### GET /v1/templates Listet die Vorlagen, die deinem Account zugewiesen sind (nur aktive). Liefert je Vorlage UUID, Name, Beschreibung, `type` (`image` oder `video`) sowie `preview_urls` (je Format die URL des Vorschau-Bildes) und `has_params` (ob für diese Vorlage bereits Vorschau-Werte hinterlegt sind). Das vollständige Parameter-Schema einer Vorlage holst du über GET /v1/templates/{id}. Query-Parameter: - limit (integer, optional): Max. Anzahl (Default 25, max. 100). (z. B. 25) - offset (integer, optional): Versatz für Paginierung (Default 0). (z. B. 0) Response: ```json { "data": [ { "id": "1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d", "name": "Event Poster Standard", "description": "Klassisches Event-Poster mit Hintergrundbild, Titel, Datum und Badge.", "type": "image", "has_params": true, "preview_urls": { "1:1": "https://api.campaignkit.ventureon.io/v1/templates/1f0c…/preview.png?ratio=1%3A1", "4:5": "https://api.campaignkit.ventureon.io/v1/templates/1f0c…/preview.png?ratio=4%3A5", "9:16": "https://api.campaignkit.ventureon.io/v1/templates/1f0c…/preview.png?ratio=9%3A16" } } ], "request_id": "…" } ``` Hinweis: Das Auflisten rendert NICHT. Die `preview_urls` sind signierte, öffentliche Bild-URLs (ohne API-Key), die du unverändert direkt im Frontend einbetten kannst (img-Tag). Gerendert wird erst beim ersten Abruf eines Bildes und danach gecacht, bis der Betreiber die Vorlage oder die Parameter ändert. Die Parameter pflegt der Betreiber je Kunde im Admin-Dashboard (du setzt sie nicht selbst). #### GET /v1/templates/{id} Liefert das einheitliche Erklärungs-Schema einer zugewiesenen Vorlage: je Parameter Name, Typ, Pflicht/optional, Bezeichnung, Beschreibung (Hinweis, was inhaltlich rein gehört), Min-/Max-Zeichen und ein Beispiel. Gedacht als Vorlage/Inspiration, damit ein KI-System die Inhalte (`params` für den Render-Call) optimal erzeugt. Ist `supports_highlight: true`, unterstützt dieser Text-Parameter Wort-Hervorhebung: Wörter im Wert, die mit doppelten eckigen Klammern umschlossen werden (`[[Wort]]`), erscheinen im Creative farbig hervorgehoben. Mehrere Wörter (`[[ganz besonders]]`) werden je Wort hervorgehoben. Beispiel-Wert: `"Jetzt [[sparen]] und anmelden"`. Bei `supports_highlight: false` werden die Klammern als normaler Text dargestellt. Path-Parameter: - id (string (UUID), erforderlich): UUID der zugewiesenen Vorlage. (z. B. 1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d) Response: ```json { "data": { "id": "1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d", "name": "Event Poster Standard", "description": "Klassisches Event-Poster …", "type": "image", "params": [ { "name": "title", "type": "text", "required": true, "label": "Titel", "description": "Kurzer, prägnanter Event-Titel ohne Datum.", "min_chars": 3, "max_chars": 40, "example": "Summer Open Air", "supports_highlight": true }, { "name": "brand_color", "type": "color", "required": false, "label": "Markenfarbe", "description": "Primärfarbe der Marke (Hex).", "min_chars": null, "max_chars": null, "example": "#0082fb", "supports_highlight": false } ] }, "request_id": "…" } ``` Hinweis: Nur dem aufrufenden Kunden zugewiesene, aktive Vorlagen sind abrufbar — sonst 404. Die Zuweisung pflegt der Betreiber im Admin-Dashboard. #### GET /v1/templates/{id}/preview Liefert die für diese Vorlage hinterlegten Parameter-Werte (vom Betreiber je Kunde im Admin-Dashboard gepflegt) plus die `preview_urls` je Format. Sind noch keine Werte gesetzt, ist `params` ein leeres Objekt und die Vorschau nutzt die Beispiel-Werte der Vorlage. Path-Parameter: - id (string (UUID), erforderlich): UUID der zugewiesenen Vorlage. (z. B. 1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d) Response: ```json { "data": { "template": "1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d", "params": { "title": "Sommerfest 2026", "brand_color": "#ff0055" }, "preview_urls": { "1:1": "https://api.campaignkit.ventureon.io/v1/templates/1f0c…/preview.png?ratio=1%3A1", "4:5": "https://api.campaignkit.ventureon.io/v1/templates/1f0c…/preview.png?ratio=4%3A5", "9:16": "https://api.campaignkit.ventureon.io/v1/templates/1f0c…/preview.png?ratio=9%3A16" } }, "request_id": "…" } ``` #### GET /v1/templates/{id}/preview.png Öffentlicher Bild-Endpunkt, direkt im Browser als Bild einbettbar (z. B. img-Tag). KEIN API-Key. autorisiert wird über die signierten Query-Parameter `c` und `k`, die in den preview_urls aus GET /v1/templates bereits enthalten sind. Du baust diese URL nicht selbst, sondern nutzt die fertige preview_url. Liefert das Bild gefüllt mit den vom Betreiber hinterlegten Parametern (sonst mit den Beispiel-Werten der Vorlage) und antwortet mit Redirect (302) auf die dauerhaft gültige Bild-URL. Performance: Das Bild wird vorab gerendert und gecacht (spätestens beim ersten Abruf). Ändern wir die Vorlage oder die hinterlegten Parameter, wird beim nächsten Abruf automatisch einmalig neu gerendert. die signierte URL bleibt unverändert gültig. Path-Parameter: - id (string (UUID), erforderlich): UUID der zugewiesenen Vorlage. (z. B. 1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d) Query-Parameter: - c (string (UUID), erforderlich): Kunden-Kennung aus der preview_url (Teil der Signatur). Bereits in der preview_url enthalten. (z. B. c0a8…) - k (string, erforderlich): Signierter Lese-Token aus der preview_url. Bereits in der preview_url enthalten. Ohne gültigen Token: 404. (z. B. 9f2b…) - ratio (enum, optional): Format: 1:1, 4:5 oder 9:16. Default 1:1. (z. B. 4:5) Hinweis: Direkt einbettbar: setze die preview_url unverändert als Bildquelle in deinem Frontend (img-Tag). Der Token ist stabil und läuft nicht ab, der API-Key bleibt geheim. Der Redirect zeigt auf eine öffentliche, unratbare Bild-URL. die Bytes liefert der Storage-CDN. #### POST /v1/templates/render Rendert eine aktive Vorlage in drei Formaten (1:1, 4:5, 9:16) und legt einen Creative-ENTWURF an. WICHTIG: Hier entsteht nur das VISUAL. Anzeigentexte (Primärer Text, Überschrift, Beschreibung), Call-to-Action, Ziel-Link und angezeigter Link gehören zur ANZEIGE und werden in POST /v1/ads gesetzt — dort wird aus diesem Entwurf das finale Meta-Creative gebaut. Die zurückgegebene `id` ist die `creative_id` für POST /v1/ads. Welche Parameter eine Vorlage erwartet, zeigt die Vorlagen-Doku (UUID eingeben). Body-Parameter: - template (string (UUID), erforderlich): UUID der Vorlage (im Admin-Dashboard unter „Vorlagen“). Nur aktive Vorlagen werden akzeptiert. (z. B. 1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d) - params (object, erforderlich): Key/Value je Vorlagen-Parameter (Inhalte des Visuals: Texte im Bild, Farben, Bild-URLs; Bild-URLs akzeptieren JPG, PNG oder WebP, ein nicht erreichbares oder ungültiges Bild wird mit einer klaren 422-Meldung abgelehnt, nicht still weggelassen). Verfügbare Keys, Typen, Pflichtfelder und Zeichengrenzen liefert GET /v1/templates/{id} (bzw. die Vorlagen-Doku unter /docs/templates?id=). In Text-Werten kannst du einzelne Wörter mit doppelten eckigen Klammern markieren (z. B. "Jetzt [[sparen]]"); diese werden im Visual hervorgehoben, sofern die Vorlage eine Hervorhebung definiert. (z. B. {"title":"Summer [[Opening]]","brand_color":"#ff0055"}) - name (string, optional): Optionaler interner Name des Creative-Entwurfs. Default: Name der Vorlage. Request: ``` POST /v1/templates/render { "template": "1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d", "params": { "title": "Summer Opening", "subtitle": "Sa. 12. Juli · 20 Uhr", "brand_color": "#ff0055" } } ``` Response: ```json { "data": { "id": "1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d", "template": "1f0c…", "type": "IMAGE", "formats": { "1:1": "https://…/rendered-creatives//1-1.png", "4:5": "https://…/rendered-creatives//4-5.png", "9:16": "https://…/rendered-creatives//9-16.png" } }, "request_id": "…" } ``` Hinweis: Die Antwort enthält die `id` (= creative_id für POST /v1/ads) und die Vorschau-Bilder je Format — aber noch KEINE `meta_creative_id`: Das finale Creative entsteht erst in POST /v1/ads (mit Texten, CTA und Link). Enthält die Vorlage einen Video-Layer, läuft das Rendern ASYNCHRON (Video-Compositing): Antwort 202 mit `type: VIDEO` und `status: PROCESSING` — danach über GET /v1/templates/render/{id} pollen, bis READY. #### GET /v1/templates/render/{id} Liefert den Status eines asynchronen Video-Renders: PROCESSING (Compositing/Upload läuft), READY (meta_creative_id vorhanden, in POST /v1/ads nutzbar) oder FAILED. {id} ist die Creative-ID aus dem 202-Response von POST /v1/templates/render. Path-Parameter: - id (string (UUID), erforderlich): Creative-ID aus dem 202-Response des Video-Renders. Request: ``` curl https://api.campaignkit.ventureon.io/v1/templates/render/1f0c… \ -H "Authorization: Bearer ak_live_..." ``` Response: ```json { "data": { "id": "1f0c…", "status": "READY", "meta_creative_id": "120210394857", "video_id": "239847562018", "output_url": "https://…/rendered-videos//output.mp4" }, "request_id": "…" } ``` Hinweis: Reporting-Scope genügt. Nur für Video-Vorlagen relevant (Bild-Renders sind synchron). ### Ads Verknüpft Ad Set + Creative zur fertigen Anzeige. Der Name wird automatisch vergeben (Naming Convention); die Ad startet als PAUSED-Entwurf. #### POST /v1/ads 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, Call-to-Action, Ziel-Link, optionaler angezeigter Link sowie Ad-Name und -Status. Aus dem Entwurf (creative_id aus POST /v1/templates/render oder POST /v1/creatives) wird damit das finale, platzierungsoptimierte Creative (Bild oder Video) gebaut und die Anzeige erstellt. Ads durchlaufen das Meta-Review. Body-Parameter: - ad_set_id (string (UUID), erforderlich): Übergeordnetes Ad Set (aus POST /v1/adsets). (z. B. a51f9c33-7e2b-4d18-8a0e-6b9c1d4f2e77) - creative_id (string (UUID), erforderlich): Creative-Entwurf aus POST /v1/templates/render (Vorlage) ODER POST /v1/creatives (eigenes Bild/Video). Aus dem Entwurf wird hier mit der Copy das finale Creative gebaut. Ist es bereits finalisiert, werden die Copy-Felder ignoriert. (z. B. 1f0c7a44-2d9e-4c61-b3a8-5e7f0c2a1b9d) - link (string (URL), optional): Echte Ziel-URL (Landingpage), auf die der Klick führt. PFLICHT, wenn creative_id ein Render-Entwurf ist. Nur die saubere URL angeben: UTM-/Tracking-Parameter (Kampagnen-, Ad-Set-, Ad-Name plus eindeutige IDs) hängt campaignKit automatisch an; KEINE eigenen Tracking-Parameter setzen. (z. B. https://acme-events.de/tickets) - display_link (string, optional): Optionaler angezeigter Link, den der Nutzer sieht, unabhängig von der echten Ziel-URL. Für lange/unleserliche Ziel-URLs eine kurze, vertrauenswürdige URL zeigen. Empfehlung für die KI: nur die Marken-Domain plus optional kurzer Pfad, ohne https:// und ohne Query-Parameter (z. B. „acme-events.de/tickets“). (z. B. acme-events.de/tickets) - call_to_action (enum, optional): Button-Text der Anzeige. Genau einer dieser Werte, Format API-Wert (angezeigter Button): SIGN_UP (Registrieren), SUBSCRIBE (Abonnieren), SEE_MORE (Mehr ansehen), DOWNLOAD (Herunterladen), APPLY_NOW (Jetzt bewerben), BOOK_NOW (Jetzt buchen), BUY_TICKETS (Tickets kaufen), CONTACT_US (Kontaktiere uns), GET_OFFER (Angebot beanspruchen), GET_QUOTE (Angebot einholen), GET_SHOWTIMES (Spielzeiten), WATCH_MORE (Details ansehen), LEARN_MORE (Mehr dazu), LISTEN_NOW (Jetzt anhören), ORDER_NOW (Jetzt bestellen). Default: LEARN_MORE (Mehr dazu). Für Event/Ticket: BUY_TICKETS. (z. B. BUY_TICKETS) - message (string, optional): In Meta „Primärer Text“: Dies ist der wichtigste Text deiner Anzeige. Er erscheint in den meisten Platzierungen (wie dem Facebook-Feed) direkt über oder unter deinem Bild oder Video. Er sollte die Hauptbotschaft vermitteln und Nutzer dazu bewegen, mehr erfahren zu wollen. Maximal 125 Zeichen. Idealerweise umfasst der Text nur 1 bis 3 Zeilen, damit er ohne „Mehr anzeigen“ vollständig lesbar ist. KEINEN Call-to-Action hier eintragen (dafür ist das CTA-Feld). Pro Anzeige genau eine Variante. (z. B. Triff Gründer und Macher beim Networking-Event in Berlin am 18. Juli ab 19 Uhr.) - headline (string, optional): In Meta „Überschrift“: Die Überschrift erscheint meist fett gedruckt neben deinem Call-to-Action-Button (z. B. „Jetzt buchen“). Sie dient dazu, das wichtigste Verkaufsargument oder den Nutzen deines Angebots kurz und prägnant hervorzuheben. Maximal 40 Zeichen. So bleibt sie auch auf kleineren Bildschirmen einzeilig und gut lesbar. KEINEN Call-to-Action hier eintragen. (z. B. Founders Night Berlin) - description (string, optional): In Meta „Beschreibung“: Dieses Feld wird oft unter der Überschrift angezeigt, ist aber nicht in allen Platzierungen sichtbar. Hier kannst du zusätzliche, nicht essenzielle Details hinzufügen, wie zum Beispiel Lieferinformationen oder ein kurzes Kunden-Testimonial. Maximal 25 Zeichen. Da dieser Text oft abgeschnitten wird, solltest du hier nur ergänzende Informationen platzieren. KEINEN Call-to-Action hier eintragen. (z. B. Fr. 18. Juli, 19 Uhr) - name (string, optional): Optionaler Anzeigenname. Ohne Angabe wird er automatisch nach Naming Convention vergeben. (z. B. Founders Night · Visual A) - status (enum, optional): PAUSED (Default, Entwurf) oder ACTIVE (direkt ausspielen nach Meta-Review). (z. B. PAUSED) Request: ``` POST /v1/ads { "ad_set_id": "a51f…", "creative_id": "1f0c…", "link": "https://acme-events.de/tickets", "display_link": "acme-events.de/tickets", "call_to_action": "BUY_TICKETS", "message": "Triff Gründer und Macher beim Networking-Event in Berlin.", "headline": "Founders Night Berlin", "description": "Fr. 18. Juli, 19 Uhr" } ``` Response: ```json { "data": { "id": "9b2e…", "meta_ad_id": "390218475610", "meta_creative_id": "120210394857", "name": "[CK] … · Ad #1", "status": "PAUSED" }, "request_id": "…" } ``` Hinweis: Aus einem Render-Entwurf (creative_id) wird hier das finale Creative gebaut (Multi-Ratio mit Placement-Regeln) und die Ad angelegt; `link` ist dann Pflicht. Das Anlegen ist nur unter einer aktiven Ziel-Kampagne möglich, sonst 409 conflict. Erfordert vollständiges Onboarding (Token verifiziert, Werbekonto, Page, Instagram-Konto, DSA/legal_name); fehlt etwas → 422 mit details.missing. #### GET /v1/ads Listet Ads inklusive ihres (gecachten) effective_status. Für den frischen Live-Status einer einzelnen Ad siehe GET /v1/status/{ad_id}. Query-Parameter: - adset_id (string (UUID), optional): Auf ein Ad Set filtern. - status (enum, optional): ACTIVE | PAUSED | ARCHIVED. - limit (integer, optional): Anzahl pro Seite (Default 25, max 100). - offset (integer, optional): Versatz für Pagination (Default 0). Response: ```json { "data": [ { "id": "9b2e1d77-4a3c-4e90-b1f2-8c7d6e5f4a3b", "ad_set_id": "a51f…", "creative_id": "1f0c…", "name": "[CK] … · Ad #1", "status": "ACTIVE", "effective_status": "ACTIVE", "disapproval_reason": null, "meta_ad_id": "390218475610" } ], "request_id": "…" } ``` #### PATCH /v1/ads/{id} Setzt den Status einer Ad (z. B. nach erfolgreichem Review aktivieren). Path-Parameter: - id (string (UUID), erforderlich): Interne Ad-ID (aus POST/GET). Body-Parameter: - status (enum, erforderlich): ACTIVE | PAUSED. Response: ```json { "data": { "id": "9b2e…", "status": "ACTIVE", "effective_status": "PENDING_REVIEW", "meta_ad_id": "390218475610" }, "request_id": "…" } ``` Hinweis: Jede Änderung (auch Aktivieren/Pausieren) erfordert eine aktive Ziel-Kampagne, sonst 409 conflict. Archivieren (DELETE) bleibt möglich. #### DELETE /v1/ads/{id} Archiviert eine Ad (status=ARCHIVED) bei uns und bei Meta. Path-Parameter: - id (string (UUID), erforderlich): Interne Ad-ID. Response: ```json { "data": { "id": "9b2e…", "status": "ARCHIVED" }, "request_id": "…" } ``` Hinweis: Archiviert statt hart zu löschen. ### Audiences Custom Audiences aus rohen CRM-Kontakten (serverseitig gehasht) und darauf basierende Lookalikes. #### GET /v1/audiences Listet die Audiences des Kunden (DB-Spiegel). Status/Größe stammen aus dem letzten Sync (last_synced_at); frischen Live-Status liefert GET /v1/audiences/{id}. Query-Parameter: - subtype (enum, optional): Filter: CUSTOM | LOOKALIKE. - status (enum, optional): Filter: active | deleted. - limit (integer, optional): Max. Treffer (Default 25, max 100). - offset (integer, optional): Offset für Pagination. Request: ``` GET /v1/audiences?subtype=CUSTOM ``` Response: ```json { "data": [ { "id": "9b2e…", "meta_audience_id": "238947561029", "subtype": "CUSTOM", "name": "Käufer 2026", "approximate_count": 4120, "ready": true, "last_synced_at": "2026-06-15T08:00:00Z" } ], "request_id": "…" } ``` #### POST /v1/audiences/custom Erstellt eine Custom Audience aus rohen CRM-Kontakten. Wir normalisieren und hashen serverseitig (SHA-256, im Speicher) — rohe PII wird nicht gespeichert. Optionaler erster Batch direkt beim Anlegen; weitere über POST /v1/audiences/{id}/contacts. Body-Parameter: - name (string, erforderlich): Name der Audience (intern und in Meta sichtbar). (z. B. Käufer 2026) - description (string, optional): Interne Beschreibung. (z. B. Alle Ticket-Käufer 2026) - customer_file_source (enum, optional): Herkunft der Daten (Default USER_PROVIDED_ONLY): USER_PROVIDED_ONLY | PARTNER_PROVIDED_ONLY | BOTH_USER_AND_PARTNER_PROVIDED. (z. B. USER_PROVIDED_ONLY) - contacts (object[], optional): Rohe CRM-Kontakte im festen Schema (max. 10.000 pro Request). Serverseitig normalisiert und gehasht (SHA-256), nichts wird gespeichert. Felder je Kontakt (mindestens eines aus email/phone/external_id): email, phone (inkl. Ländervorwahl), first_name, last_name, city, state, zip, country (ISO-2), year_of_birth (Zahl), gender (m|f), external_id (nicht gehasht). (z. B. [{"email":"max@example.com","phone":"+4915112345678","first_name":"Max","zip":"50667","country":"DE"}]) Request: ``` POST /v1/audiences/custom { "name": "Käufer 2026", "customer_file_source": "USER_PROVIDED_ONLY", "contacts": [ { "email": "max@example.com", "phone": "+4915112345678", "first_name": "Max", "last_name": "Mustermann", "zip": "50667", "country": "DE" } ] } ``` Response: ```json { "data": { "id": "9b2e…", "meta_audience_id": "238947561029", "subtype": "CUSTOM", "contacts_received": 1 }, "request_id": "…" } ``` Hinweis: DSGVO: nur mit Einwilligung verarbeiten. Pro Request max. 10.000 Kontakte; größere Listen paginieren. Custom Audiences setzen voraus, dass (a) die Custom-Audience-Nutzungsbedingungen im Werbekonto akzeptiert sind UND (b) das Werbekonto dafür freigeschaltet ist. Sehr neue Werbekonten 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. Abhilfe: Nutzungsbedingungen akzeptieren und/oder zuerst eine Anzeige schalten und das Konto reifen lassen, dann erneut versuchen. #### POST /v1/audiences/lookalike Erzeugt eine Lookalike-Audience aus einer eigenen Custom Audience für ein Land mit wählbarem Ähnlichkeitsgrad (empfohlen 1–10 %). Die Quelle sollte ≥ 100 Treffer haben. Body-Parameter: - name (string, erforderlich): Name der Lookalike-Audience. (z. B. LAL Käufer 2026 DE 3%) - source_audience_id (string (UUID), erforderlich): Quell-Custom-Audience (interne UUID aus POST /v1/audiences/custom). (z. B. 9b2e1f7c-…) - country (string, erforderlich): Zielland als ISO-2 (z. B. DE). (z. B. DE) - ratio (number (0.01–0.20), erforderlich): Ähnlichkeitsgrad (1–20 %, empfohlen 1–10 %). (z. B. 0.03) Request: ``` POST /v1/audiences/lookalike { "name": "LAL Käufer 2026 DE 3%", "source_audience_id": "9b2e1f7c-…", "country": "DE", "ratio": 0.03 } ``` Response: ```json { "data": { "id": "7a1c…", "meta_audience_id": "238947561777", "subtype": "LOOKALIKE", "source_audience_id": "9b2e1f7c-…" }, "request_id": "…" } ``` Hinweis: Wie bei Custom Audiences kann Meta 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. #### GET /v1/audiences/{id} Liefert eine Audience inkl. frischem Status von Meta: operation_status, delivery_status, approximate_count und ready (true = fertig gebaut und in Ad-Sets nutzbar). Path-Parameter: - id (string (UUID), erforderlich): Interne Audience-ID. Response: ```json { "data": { "id": "9b2e…", "meta_audience_id": "238947561029", "subtype": "CUSTOM", "name": "Käufer 2026", "approximate_count": 4120, "operation_status_code": 200, "delivery_status_code": 200, "ready": true }, "request_id": "…" } ``` Hinweis: approximate_count kann direkt nach dem Upload noch 0 oder veraltet sein (die Population dauert). #### PATCH /v1/audiences/{id} Ändert Name und/oder Beschreibung einer Audience. Path-Parameter: - id (string (UUID), erforderlich): Interne Audience-ID. Body-Parameter: - name (string, optional): Neuer Name. - description (string, optional): Neue Beschreibung. Response: ```json { "data": { "id": "9b2e…", "name": "Käufer 2026 (aktiv)", "subtype": "CUSTOM" }, "request_id": "…" } ``` #### DELETE /v1/audiences/{id} Löscht eine Audience bei Meta und markiert den Spiegel als deleted. Path-Parameter: - id (string (UUID), erforderlich): Interne Audience-ID. Response: ```json { "data": { "id": "9b2e…", "status": "deleted" }, "request_id": "…" } ``` #### POST /v1/audiences/{id}/contacts Fügt einer Custom Audience weitere Kontakte hinzu (append). Rohe Kontakte im festen Schema, serverseitig gehasht. Max. 10.000 pro Request. Path-Parameter: - id (string (UUID), erforderlich): Interne Audience-ID (nur CUSTOM). Body-Parameter: - contacts (object[], erforderlich): Rohe CRM-Kontakte im festen Schema (max. 10.000 pro Request). Serverseitig normalisiert und gehasht (SHA-256), nichts wird gespeichert. Felder je Kontakt (mindestens eines aus email/phone/external_id): email, phone, first_name, last_name, city, state, zip, country (ISO-2), year_of_birth, gender (m|f), external_id (nicht gehasht). (z. B. [{"email":"erika@example.com","country":"DE"}]) Request: ``` POST /v1/audiences/9b2e…/contacts { "contacts": [ { "email": "erika@example.com", "country": "DE" } ] } ``` Response: ```json { "data": { "id": "9b2e…", "contacts_received": 1, "contacts_invalid": 0 }, "request_id": "…" } ``` #### DELETE /v1/audiences/{id}/contacts Entfernt Kontakte aus einer Custom Audience (DSGVO-Opt-out, Löschbegehren). Gleiches Kontakt-Schema wie beim Hinzufügen. Path-Parameter: - id (string (UUID), erforderlich): Interne Audience-ID (nur CUSTOM). Body-Parameter: - contacts (object[], erforderlich): Rohe CRM-Kontakte (festes Schema) zum Entfernen. Identifikation über dieselben gehashten Felder wie beim Hinzufügen. (z. B. [{"email":"erika@example.com"}]) Response: ```json { "data": { "id": "9b2e…", "contacts_removed": 1 }, "request_id": "…" } ``` ### Targeting Lookup-Helfer, um gültige Geo- und Interessen-Keys für das Targeting zu finden. #### GET /v1/targeting/geo?q= Sucht Städte und liefert die von Meta erwarteten City-Keys (DACH), die direkt im Ad-Set-Targeting (geo_locations.cities) verwendet werden. Query-Parameter: - q (string, erforderlich): Suchbegriff, z. B. „Berlin“. (z. B. Berlin) - country_code (string, optional): Einschränken auf DE/AT/CH. - limit (integer, optional): Max. Treffer. Request: ``` GET /v1/targeting/geo?q=Berlin&country_code=DE ``` Response: ```json { "data": [ { "key": "2972535", "name": "Berlin", "type": "city", "country_code": "DE" } ] } ``` #### GET /v1/targeting/interests?q= Sucht Interessen-Targeting-Optionen und liefert deren IDs für die Verwendung im Ad-Set-Targeting. Query-Parameter: - q (string, erforderlich): Suchbegriff, z. B. „Live-Musik“. (z. B. Live-Musik) - limit (integer, optional): Max. Treffer. Request: ``` GET /v1/targeting/interests?q=Live-Musik ``` Response: ```json { "data": [ { "id": "6003139266461", "name": "Live music", "audience_size": 248000000 } ] } ``` ### Reporting Normalisierte Performance-Metriken aus täglich gesyncten Snapshots. Die Kundenplattform verknüpft sie mit eigenen Verkaufsdaten. #### GET /v1/insights 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. Geldwerte in Major-Units der Konto-Währung. Daten aus täglichen DB-Snapshots, kein Live-Call zur Meta-API. Die Kampagnen-Ebene ist nicht verfügbar. Query-Parameter: - level (enum, erforderlich): adset oder ad. Ebene der zurückgegebenen Entities. Die Kampagnen-Ebene ist nicht verfügbar. (z. B. adset) - entity_id (string (UUID), optional): Interne UUID eines Ad-Sets (level=adset) oder einer Ad (level=ad), aus GET /v1/adsets bzw. /v1/ads. Ohne Angabe werden alle Entities der Ebene zurückgegeben. (z. B. a1b2c3d4-e5f6-7890-abcd-ef1234567890) - adset_id (string (UUID), optional): 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. (z. B. a51f9c33-7e2b-4d18-8a0e-6b9c1d4f2e77) - date_range (string | object, optional): Preset: 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 Objekt {since: "YYYY-MM-DD", until: "YYYY-MM-DD"}. Default: last_30d. (z. B. last_30d) - fields (string (CSV), optional): Auswahl der Kern-Metriken: spend, impressions, reach, clicks, ctr, cpc, cpm, conversions, conversion_value, roas, cpa. Default: alle. (z. B. spend,impressions,clicks,ctr,cpc,roas) - breakdowns (string (CSV), optional): Aufschlüsselung nach genau einem gesyncten Set: age, gender, country, publisher_platform, platform_position, impression_device, device_platform oder die Kombination age,gender. Reihenfolge wie angegeben. (z. B. age,gender) Request: ``` # Metriken eines Ad-Sets GET /v1/insights?level=adset&entity_id=a51f9c33-7e2b-4d18-8a0e-6b9c1d4f2e77&date_range=last_30d # Metriken aller Ads dieses Ad-Sets (ohne deren IDs zu kennen) GET /v1/insights?level=ad&adset_id=a51f9c33-7e2b-4d18-8a0e-6b9c1d4f2e77&date_range=last_30d ``` Response: ```json { "data": [ { "id": "9f3c1d4f-2e77-4a18-8a0e-6b9c1d4f2e77", "adset_id": "a51f9c33-7e2b-4d18-8a0e-6b9c1d4f2e77", "meta_id": "604839271650", "spend": 1928.00, "impressions": 187400, "reach": 92100, "clicks": 5082, "ctr": 2.71, "cpc": 0.38, "cpm": 10.29, "conversions": 246, "conversion_value": 9832.80, "roas": 5.1, "cpa": 7.84 } ] } ``` Hinweis: level=ad-Antworten enthalten zusätzlich adset_id (zum Gruppieren). Datenfrische: tägliche Snapshots, Conversions werden über ein 7-Tage-Fenster nachträglich aktualisiert (Backfill). Mit breakdowns enthält jede Zeile zusätzlich ein breakdown-Objekt (z. B. {age, gender}). reach ist eine Unique-Metrik und wird NICHT über Tage aufsummiert (das würde Personen mehrfach zählen): bei einem Zeitraum über mehrere Tage ist reach die höchste Einzeltags-Reichweite (belastbare Untergrenze), bei einem einzelnen Tag exakt. Alle übrigen Mengen-Metriken (spend, impressions, clicks, conversions, conversion_value) sind additiv. ### Status / Webhooks Review-/Lieferstatus live abfragen (Pull) oder per registrierter Webhook-URL empfangen (Push). #### GET /v1/status/{ad_id} Liefert den aktuellen effective_status einer Ad — live aus der Meta-API geholt und serverseitig zwischengespeichert. Bei DISAPPROVED wird der Ablehnungsgrund mitgegeben, damit die Kundenplattform ihn anzeigen und einen Retry ermöglichen kann. Path-Parameter: - ad_id (string (UUID), erforderlich): Interne Ad-ID (aus POST /v1/ads). (z. B. 9b2e7c11-3a4d-4f88-bb20-1c6e8a5d2f90) Request: ``` GET /v1/status/9b2e7c11-3a4d-4f88-bb20-1c6e8a5d2f90 ``` Response: ```json { "data": { "ad_id": "9b2e7c11-3a4d-4f88-bb20-1c6e8a5d2f90", "effective_status": "DISAPPROVED", "disapproval_reason": "Text-Overlay zu groß" }, "request_id": "…" } ``` Hinweis: effective_status ist auf ACTIVE | PAUSED | PENDING_REVIEW | DISAPPROVED | ARCHIVED normalisiert. disapproval_reason ist nur bei DISAPPROVED gesetzt, sonst null. #### GET /v1/webhooks Listet die registrierten Webhook-Endpunkte des Kunden mit URL, abonnierten Events, Status und Zustell-Statistik (letzte Zustellung, Fehlerzähler). Das signing_secret wird NICHT zurückgegeben (nur einmalig bei der Registrierung). Scope reporting genügt. Request: ``` GET /v1/webhooks ``` Response: ```json { "data": [ { "id": "wh_5d33…", "url": "https://kunde.de/hooks/campaignkit", "events": ["ad.status_changed"], "status": "active", "last_delivery_at": "2026-06-18T10:12:00Z", "failure_count": 0, "created_at": "2026-06-01T08:00:00Z" } ], "request_id": "…" } ``` #### POST /v1/webhooks Registriert eine HTTPS-URL, an die Statusänderungen gepusht werden (z. B. Anzeige genehmigt/abgelehnt). So muss die Kundenplattform nicht pollen. Die Antwort enthält EINMALIG ein signing_secret — damit jede Zustellung über den Header X-CampaignKit-Signature verifiziert werden kann. Body-Parameter: - url (string (URL), erforderlich): HTTPS-Endpunkt der Kundenplattform (http wird abgelehnt). (z. B. https://kunde.de/hooks/campaignkit) - events (string[], optional): Abonnierte Ereignisse. Default: ["ad.status_changed"] (aktuell einziges Event). (z. B. ["ad.status_changed"]) Request: ``` POST /v1/webhooks { "url": "https://kunde.de/hooks/campaignkit", "events": ["ad.status_changed"] } ``` Response: ```json { "data": { "id": "wh_5d33…", "url": "https://kunde.de/hooks/campaignkit", "events": ["ad.status_changed"], "active": true, "signing_secret": "whsec_… (nur hier einmalig)" }, "request_id": "…" } ``` Hinweis: signing_secret wird nur in dieser Antwort zurückgegeben und ist danach nicht mehr abrufbar — sicher speichern. Zustellungen tragen die Header X-CampaignKit-Event, X-CampaignKit-Delivery (Dedup) und X-CampaignKit-Signature: t=,v1=. Verifikation: HMAC-SHA256 über "." mit dem signing_secret bilden und mit v1 vergleichen. ### Plan & Abrechnung Vertragszustand und voraussichtliche Gebühr lesen, Konto selbst deaktivieren und jederzeit an die Rechnungen kommen. Es gibt keine Tarifwahl: die Stufe ergibt sich rückwirkend aus dem realen Ad-Spend. #### GET /v1/plan Liefert den Vertragszustand (active | deactivated | past_due), ob Schreibzugriffe möglich sind, das laufende Zyklusfenster, den bisherigen Spend der verwalteten Kampagnen, die voraussichtliche Gebühr (min über Stufen) sowie die Tarif-Staffel. Response: ```json { "data": { "account_status": "active", "can_write": true, "current_cycle": { "start": "2026-06-18", "end": "2026-07-18" }, "spend_to_date_cents": 250000, "projected": { "tier": "starter", "fix_cents": 9900, "media_fee_cents": 15000, "total_cents": 24900 }, "last_billed": { "tier": "starter", "total_cents": 24900 }, "tiers": [ { "key": "starter", "name": "Starter", "fix_cents": 9900, "media_fee_pct": 0.06, "spend_min_cents": 0, "spend_max_cents": 800000 }, { "key": "growth", "name": "Growth", "fix_cents": 25900, "media_fee_pct": 0.04, "spend_min_cents": 800000, "spend_max_cents": 1600000 }, { "key": "scale", "name": "Scale", "fix_cents": 49900, "media_fee_pct": 0.025, "spend_min_cents": 1600000, "spend_max_cents": null } ] }, "request_id": "…" } ``` Hinweis: Beträge in Cent, netto (zzgl. 19 % USt auf der Rechnung). Die Stufe wird am Zyklusende automatisch aus dem Gesamt-Spend bestimmt; solange aktiv gilt mindestens die Starter-Stufe (99 €). Scope reporting. #### GET /v1/plan/usage Zeigt das laufende Zyklusfenster (am Vertrags-Starttag verankert), den bisher angefallenen Spend der über campaignKit verwalteten Kampagnen und die voraussichtliche Gebühr. KEIN Limit — der Spend wird nie gedeckelt. Response: ```json { "data": { "cycle_start": "2026-06-18", "cycle_end": "2026-07-18", "spend_to_date_cents": 250000, "projected_tier": "starter", "projected_fee_cents": 24900 }, "request_id": "…" } ``` Hinweis: Scope reporting. #### POST /v1/account/deactivate Deaktiviert das Konto im Selbst-Service: Schreibzugriffe (neue oder aktive Ads) werden gesperrt, Lesen (Status/Insights) bleibt möglich. Der laufende Zyklus wird am Zyklusende noch abgerechnet, danach 0 €, bis ein Admin reaktiviert. Request: ``` POST /v1/account/deactivate ``` Response: ```json { "data": { "account_status": "deactivated" }, "request_id": "…" } ``` Hinweis: Reaktivierung erfolgt NICHT per API, sondern auf Anfrage beim Anbieter per E-Mail an kontakt@ventureon.io. Idempotent: erneuter Aufruf im deaktivierten Zustand liefert account_status=deactivated.