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
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
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
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
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
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
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
| Methode | Pfad | Auth / Limit | Felder & Antworten |
|---|---|---|---|
| POST | /api/integrations/social/connect/exchange | ohne 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/me | X-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/verify | X-API-Key | Website-Nachweis prüfen (Datei /.well-known/dimedial-connect.txt oder DNS-TXT) → {status, methode, fehler, …} |
| POST | /api/integrations/social/posts | X-API-Key · Idempotency-Key | Multipart 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-Key | Status, entwurf, fehlt[], vorschau{vorschau_url, vorschau_text}, veroeffentlichungen[{kanal, url}] · 404 bei unbekannter oder fremder ID |
| GET | /api/integrations/social/status | X-API-Key | Verbindungsprüfung: Konto (maskiert), Voraussetzungen, bereit, offene Freigaben |
| GET | /api/integrations/social/openapi.json | öffentlich | Versionierter 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_freigabeNode / 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ügbarfreigegeben / entschiedenKunde hat freigegeben bzw. Zeitpunkt gewähltveroeffentlicht / teilweiseLive – mit veroeffentlichungen[] je Kanal; teilweise = mindestens ein Kanal fehlgeschlagenverworfenKunde hat den Entwurf abgelehntfehltNicht möglich, Gründe in fehlt[]: kanal, kontingent, fotos, story_kanalfehler / angehaltenTechnischer Fehler bzw. von DIMEDIAL angehalten
HTTP-Fehler
400Exchange: Code ungültig, abgelaufen, bereits benutzt oder site/state passen nicht401Schlüssel fehlt, ungültig oder widerrufen → im CMS „Schlüssel ungültig – bitte neu verbinden“ anzeigen404Gü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 Authentifizierungsfehler409Nur 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-Keyje Schlüssel → gleicheauftrag_id, Antwort mitduplikat: 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_freigabeenthält der Statusvorschau.vorschau_url(signiert, ohne Login abrufbar, mindestens 7 Tage) undvorschau_text– zeige sie im CMS an. - Polling: alle 30–60 Sekunden, Abbruch bei
veroeffentlicht,teilweise,verworfen,fehlt,fehler. Bei 429Retry-Afterbeachten.
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
