DIMEDIAL Website-Connector · Entwickler-Dokumentation · API v1.1

Eigene Website oder
anderes CMS anbinden.

Deine Website übergibt Bild und Text, Robbie gestaltet den Beitrag, der Kunde gibt im Kundenbereich oder per WhatsApp frei. Die Verbindung entsteht ohne Schlüssel-Kopieren: Ein Klick auf der Website, Anmeldung bei DIMEDIAL, Zustimmung – der Schlüssel landet sicher in deinem Backend.

Grundsatz: Der Schlüssel gehört ins Backend.

Nie im Browser

Der Schlüssel dsk_… wird genau einmal in der Exchange-Antwort ausgeliefert – an deinen Server. Er gehört in einen Secret-Store oder eine verschlüsselte Konfiguration, nie in Frontend-Code, Themes oder Logs.

Nur zwei Rechte

posts:write (Beiträge übergeben) und posts:read (Status abfragen). Keine Veröffentlichung, kein Zugriff auf Konto, Dateien oder Kanäle.

Jederzeit widerrufbar

Der Kunde sieht jede Verbindung im Kundenbereich (Quelle, Website, zuletzt genutzt, Aufrufe) und kann sie widerrufen – danach antwortet jede Anfrage mit 401.

Ablauf (Sequenz)

  1. 1

    Button auf deiner Website

    „Mit DIMEDIAL verbinden“ öffnet ein Popup (520×720) oder – wenn Popups blockiert sind – dieselbe URL im Vollfenster:

    https://dimedial.studio/kunden/connect?site=<host>&label=<Bezeichnung>&state=<zufall>&return_url=https://<host>/…/callback
  2. 2

    Kunde meldet sich an

    Login oder Registrierung (30 Tage kostenlos, ohne Zahlungsdaten, keine automatische Verlängerung) direkt im selben Fenster. Die Anfrage überlebt Login, Registrierung und E-Mail-Bestätigung (signierter Kontext, 2 Stunden).

  3. 3

    Voraussetzungen & Zustimmung

    Kanal verbinden (inline erledigbar); WhatsApp ist optional – die Freigabe läuft im Kundenbereich, auf Wunsch zusätzlich per WhatsApp. Die Zustimmungsseite zeigt Zielhost, Rückgabe-URL und die Rechte des Schlüssels (posts:write, posts:read). Verbinden ist auch ohne erfüllte Voraussetzungen möglich – Aufträge enden dann im Status „fehlt“ mit Gründen.

  4. 4

    Rückgabe mit Einmal-Code

    Redirect auf return_url?code=…&state=… (bei Abbruch ?error=abgebrochen&state=…). Der Code ist ≥ 32 Zeichen, 5 Minuten gültig, genau einmal einlösbar und an Website + state gebunden. return_url muss https sein und auf die Website (oder eine Subdomain) zeigen – sonst Fehlerseite ohne Redirect.

  5. 5

    Code gegen Schlüssel tauschen – im Backend

    Dein Server ruft POST /api/integrations/social/connect/exchange mit {code, site, state}. Nur diese Antwort enthält den Klartext-Schlüssel dsk_…. Speichere ihn serverseitig (verschlüsselt/Secret-Store), zeige ihn im CMS maskiert.

  6. 6

    Beiträge übergeben

    POST /api/integrations/social/posts mit Bild + Text → 202 mit auftrag_id. Status pollen; ab entwurf_zur_freigabe liefert der Status eine signierte Vorschau (vorschau_url, ≥ 7 Tage) und freigabe_via (portal | whatsapp) sowie freigabe_url. Veröffentlicht wird ausschließlich nach ausdrücklicher Freigabe des Kunden – im Kundenbereich oder per WhatsApp.

Popup blockiert? Öffne dieselbe URL im Vollfenster – der Callback erkennt über window.opener, ob er sich schließen oder zurück in deine Einstellungen leiten soll. Nach erfolgreicher Verbindung zeigst du den Schlüssel im CMS nur maskiert (z. B. dsk_ab12…) und prüfst ihn mit GET /keys/me.

Endpunkte

MethodePfadAuth / LimitFelder & Antworten
POST/api/integrations/social/connect/exchangeohne Schlüssel · 10/Min/IP{code, site, state} → 200 {api_key, label, site, scopes, account{whatsapp_verified, channels, plan, quota_left}, verifikation} · 400 Code ungültig oder abgelaufen · 429
GET/api/integrations/social/keys/meX-API-Key{label, site, source, created_at, last_used_at, calls, scopes, verifikation, whatsapp_verified, channels, plan, quota_left} · 401 wenn widerrufen
POST/api/integrations/social/keys/me/verifyX-API-KeyWebsite-Nachweis prüfen (Datei /.well-known/dimedial-connect.txt oder DNS-TXT) → {status, methode, fehler, …}
POST/api/integrations/social/postsX-API-Key · Idempotency-KeyMultipart image (JPEG/PNG/WebP ≤ 10 MB), text (≤ 2000), publish_type feed|story → 202 {auftrag_id, status: wird_erstellt, freigabe_via: whatsapp} · 20/Std/Schlüssel
GET/api/integrations/social/posts/{auftrag_id}X-API-KeyStatus, entwurf, fehlt[], vorschau{vorschau_url, vorschau_text}, veroeffentlichungen[{kanal, url}] · 404 bei unbekannter oder fremder ID
GET/api/integrations/social/statusX-API-KeyVerbindungsprüfung: Konto (maskiert), Voraussetzungen, bereit, offene Freigaben
GET/api/integrations/social/openapi.jsonöffentlichVersionierter OpenAPI-Auszug (aus der echten Implementierung erzeugt)

Basis-URL: https://dimedial.studio. Alle Zeiten UTC/ISO 8601. Auth per Header X-API-Key: dsk_… (alternativ Authorization: Bearer).

Beispiele (vollständig, mit Platzhaltern)

Ersetze www.meine-website.de durch deinen Host. Es ist kein echter Schlüssel enthalten – der Schlüssel entsteht erst beim Exchange auf deinem Server.

Python / FastAPI

# FastAPI – Callback nimmt den Einmal-Code entgegen und tauscht ihn SERVERSEITIG gegen den Schlüssel
import os, secrets, httpx
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse

DIMEDIAL = "https://dimedial.studio"
SITE = "www.meine-website.de"                      # genau der Host, der im Connect-Link als site steht
app = FastAPI()

@app.get("/admin/dimedial/connect")               # Button-Ziel: state erzeugen, Popup/Vollfenster öffnen
def connect(request: Request):
    state = secrets.token_urlsafe(24)
    request.session["dimedial_state"] = state      # oder DB – muss beim Callback geprüft werden
    url = (f"{DIMEDIAL}/kunden/connect?site={SITE}&label=Meine%20Website"
           f"&state={state}&return_url=https://{SITE}/admin/dimedial/callback")
    return RedirectResponse(url)

@app.get("/admin/dimedial/callback")
async def callback(request: Request, code: str = "", state: str = "", error: str = ""):
    if error:                                      # z. B. error=abgebrochen
        return HTMLResponse("<p>Verbindung abgebrochen.</p>")
    if state != request.session.get("dimedial_state"):
        raise HTTPException(400, "state passt nicht")
    async with httpx.AsyncClient(timeout=10) as c:
        r = await c.post(f"{DIMEDIAL}/api/integrations/social/connect/exchange",
                         json={"code": code, "site": SITE, "state": state})
    if r.status_code != 200:                       # 400 = ungültig/abgelaufen/bereits benutzt
        raise HTTPException(400, r.json().get("detail", "Code ungültig oder abgelaufen"))
    d = r.json()
    save_secret("DIMEDIAL_API_KEY", d["api_key"])  # Secret-Store / verschlüsselt – NIE an den Browser
    # Popup: Opener informieren und schließen; Vollfenster: zurück in die Einstellungen
    return HTMLResponse("<script>if(window.opener){window.opener.postMessage({dimedial:'verbunden'}, location.origin);window.close();}else{location='/admin/einstellungen';}</script>")

# Beitrag übergeben (serverseitig, z. B. beim Veröffentlichen eines Artikels)
async def beitrag_uebergeben(artikel_id: int, bild: bytes, titel: str, teaser: str):
    headers = {"X-API-Key": os.environ["DIMEDIAL_API_KEY"],
               "Idempotency-Key": f"meine-website-posts-{artikel_id}"}   # Retry/Doppelklick = derselbe Auftrag
    async with httpx.AsyncClient(timeout=30) as c:
        r = await c.post(f"{DIMEDIAL}/api/integrations/social/posts", headers=headers,
                         files={"image": ("bild.jpg", bild, "image/jpeg")},
                         data={"text": f"{titel}\n{teaser}"[:2000], "publish_type": "feed"})
    r.raise_for_status()                           # 202; 401 → neu verbinden; 409 → später erneut
    return r.json()["auftrag_id"]

async def status(auftrag_id: str) -> dict:         # Polling: alle 30–60 s, bis veroeffentlicht/verworfen/fehlt
    async with httpx.AsyncClient(timeout=10) as c:
        r = await c.get(f"{DIMEDIAL}/api/integrations/social/posts/{auftrag_id}",
                        headers={"X-API-Key": os.environ["DIMEDIAL_API_KEY"]})
    r.raise_for_status()
    return r.json()        # .get("vorschau", {}).get("vorschau_url") ab entwurf_zur_freigabe

Node / Express

// Node/Express – gleiche Logik, Schlüssel bleibt im Backend
import express from "express";
import crypto from "node:crypto";
const DIMEDIAL = "https://dimedial.studio", SITE = "www.meine-website.de";
const app = express();

app.get("/admin/dimedial/connect", (req, res) => {
  const state = crypto.randomBytes(24).toString("base64url");
  req.session.dimedialState = state;
  const u = new URL(DIMEDIAL + "/kunden/connect");
  u.search = new URLSearchParams({ site: SITE, label: "Meine Website", state, return_url: `https://${SITE}/admin/dimedial/callback` });
  res.redirect(u);                                  // Frontend: window.open(url, "dimedial", "width=520,height=720") – Fallback location.href
});

app.get("/admin/dimedial/callback", async (req, res) => {
  const { code, state, error } = req.query;
  if (error) return res.send("Verbindung abgebrochen.");
  if (state !== req.session.dimedialState) return res.status(400).send("state passt nicht");
  const r = await fetch(DIMEDIAL + "/api/integrations/social/connect/exchange", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code, site: SITE, state }) });
  if (!r.ok) return res.status(400).send((await r.json()).detail || "Code ungültig oder abgelaufen");
  const d = await r.json();
  await saveSecret("DIMEDIAL_API_KEY", d.api_key);  // nie an den Browser, im CMS nur maskiert (dsk_abcd…)
  res.send("<script>if(window.opener){window.opener.postMessage({dimedial:'verbunden'},location.origin);window.close();}else{location='/admin/einstellungen';}</script>");
});

export async function beitragUebergeben(id, bildBuffer, titel, teaser) {
  const fd = new FormData();
  fd.append("image", new Blob([bildBuffer], { type: "image/jpeg" }), "bild.jpg");
  fd.append("text", `${titel}\n${teaser}`.slice(0, 2000));
  fd.append("publish_type", "feed");
  const r = await fetch(DIMEDIAL + "/api/integrations/social/posts", { method: "POST", body: fd, headers: { "X-API-Key": process.env.DIMEDIAL_API_KEY, "Idempotency-Key": `meine-website-posts-${id}` } });
  if (r.status === 401) throw new Error("Schlüssel ungültig – bitte neu verbinden");
  if (r.status === 409) throw new Error("Es wartet bereits eine Freigabe – später erneut");
  if (!r.ok) throw new Error(`DIMEDIAL ${r.status}`);
  return (await r.json()).auftrag_id;               // Status: GET /api/integrations/social/posts/<auftrag_id>
}

Statuswerte & Fehlercodes

Status eines Auftrags

  • wird_erstelltRobbie baut den Entwurf (Sekunden bis wenige Minuten)
  • entwurf_zur_freigabeVorschau wartet auf den Kunden (Kundenbereich; bei verbundenem WhatsApp zusätzlich dort); vorschau_url verfügbar
  • freigegeben / entschiedenKunde hat freigegeben bzw. Zeitpunkt gewählt
  • veroeffentlicht / teilweiseLive – mit veroeffentlichungen[] je Kanal; teilweise = mindestens ein Kanal fehlgeschlagen
  • verworfenKunde hat den Entwurf abgelehnt
  • fehltNicht möglich, Gründe in fehlt[]: kanal, kontingent, fotos, story_kanal
  • fehler / angehaltenTechnischer Fehler bzw. von DIMEDIAL angehalten

HTTP-Fehler

  • 400Exchange: Code ungültig, abgelaufen, bereits benutzt oder site/state passen nicht
  • 401Schlüssel fehlt, ungültig oder widerrufen → im CMS „Schlüssel ungültig – bitte neu verbinden“ anzeigen
  • 404Gültiger Schlüssel, aber die Auftrags-ID ist unbekannt oder gehört zu einem anderen Konto. Es werden bewusst keine Metadaten verraten – 404 ist kein Authentifizierungsfehler
  • 409Nur bei verbundenem WhatsApp: dort wartet bereits ein Beitrag auf Freigabe – kein Entwurf angelegt; nach Freigabe/Verwerfen erneut versuchen. Ein fehlendes WhatsApp blockiert nicht (Freigabe im Kundenbereich)
  • 413 / 415Bild zu groß (> 10 MB) / kein lesbares Bild (JPEG, PNG, WebP)
  • 422Ungültige Felder (publish_type, Text > 2000 Zeichen, Code zu kurz …)
  • 429Limit: 20 Aufträge je Stunde und Schlüssel bzw. 10 Exchange-Versuche je Minute und IP – Retry-After beachten, exponentiell warten

Idempotenz, Termine, Vorschau

  • Retry/Doppelklick = derselbe Auftrag. Gleicher Idempotency-Key je Schlüssel → gleiche auftrag_id, Antwort mit duplikat: true. Empfehlung: <system>-posts-<id>.
  • Absichtlich neu übergeben = neue Idempotenz-ID (z. B. …-v2). Erst dann entsteht ein neuer Entwurf und eine neue Freigabeanfrage.
  • Übergabetermin ≠ Veröffentlichungstermin. Wann dein System übergibt, bestimmst du. Wann veröffentlicht wird, entscheidet der Kunde bei der Freigabe im Kundenbereich oder per WhatsApp („Jetzt“ oder Zeitpunkt).
  • Vorschau: ab entwurf_zur_freigabe enthält der Status vorschau.vorschau_url (signiert, ohne Login abrufbar, mindestens 7 Tage) und vorschau_text – zeige sie im CMS an.
  • Polling: alle 30–60 Sekunden, Abbruch bei veroeffentlicht, teilweise, verworfen, fehlt, fehler. Bei 429 Retry-After beachten.

Website-Verifikation (freiwillig, serverseitig)

Der Nachweis bestätigt, dass die verbundene Website wirklich zu dir gehört – ein gleichlautender site-Wert allein genügt dafür nicht. DIMEDIAL prüft nur über https, löst den Host auf, blockiert private/interne Adressen, folgt höchstens 3 Weiterleitungen auf demselben Host und liest maximal 4 KB.

Automatisch im Connect-Flow

Liefert deine Website während des Flows unter /.well-known/dimedial-connect.txt den aktuellen state, ist der Schlüssel sofort „Website bestätigt“.

Datei mit Token

Die Exchange-Antwort enthält verifikation.token (dmv_…). Datei ausliefern, dann POST /keys/me/verify aufrufen. Der Kunde kann im Kundenbereich jederzeit „Jetzt prüfen“.

DNS (Alternative)

TXT-Eintrag _dimedial.<host> (oder am Host selbst) mit dimedial-verify=<token>. Nur nötig, wenn keine Datei ausgeliefert werden kann.

Häufige Fragen

Kurz erklärt

Häufige Fragen.

Brauche ich für jede Website einen eigenen Schlüssel?+

Ja – ein Schlüssel ist an eine Website (site) gebunden und einzeln widerrufbar. Ein Konto kann bis zu 5 aktive Schlüssel haben; alle teilen sich das Monatskontingent des Kontos.

Was passiert bei Retry oder Doppelklick?+

Mit demselben Idempotency-Key liefert die Schnittstelle denselben Auftrag zurück (duplikat: true) – es entsteht kein zweiter Entwurf. Willst du denselben Beitrag absichtlich erneut übergeben (neue Generation), verwende eine neue Idempotenz-ID, z. B. mit Suffix -v2.

Ist ein Termin im CMS ein Veröffentlichungstermin?+

Nein. Ein lokales Vormerken oder ein geplanter Zeitpunkt im CMS steuert nur, wann dein System den Beitrag übergibt. Der tatsächliche Veröffentlichungszeitpunkt wird vom Kunden bei der Freigabe per WhatsApp bestimmt.

Kann ich die Website-Verifikation weglassen?+

Ja, sie ist freiwillig: Der Schlüssel funktioniert auch ohne. Der Nachweis zeigt dem Kunden im Kundenbereich „Website bestätigt“. Am einfachsten liefert dein Adapter unter /.well-known/dimedial-connect.txt den Token aus der Exchange-Antwort (verifikation.token) oder bereits während des Connect-Flows den state – dann ist die Website sofort bestätigt.

Kann ich die Schnittstelle an ChatGPT, Claude oder andere Agenten anbinden?+

Vorbereitet, aber noch kein fertiges Produkt: Der OpenAPI-Auszug beschreibt Routen und Felder maschinenlesbar und ist die Grundlage für spätere Agenten-Anbindungen (Actions/MCP). Heute gibt es keine freigegebene Agenten-Integration.

Was kostet das?+

Die Schnittstelle ist Teil des DIMEDIAL-Kontos. Neue Kunden testen 30 Tage kostenlos ohne Zahlungsdaten und ohne automatische Verlängerung; danach gelten die Social-Media-Tarife. Jeder übergebene Beitrag zählt wie ein Sofort-Beitrag auf das Monatskontingent.

Fragen zur Anbindung: info@dimedial.de · Kundenbereich: Social Media → Schnittstelle & API

Gute Nachricht: Auf dieser Website gibt es kein Tracking und keine Marketing-Cookies. Nur im Kundenbereich setzen wir nach dem Login ein technisch notwendiges Cookie. Details in der Datenschutzerklärung.