Übersicht

20. API

PRONTO hat keine allgemeine REST-API mit API-Keys für freien CRM-Datenzugriff (siehe 21-api-keys.md). Was existiert, sind öffentliche, unauthentifizierte Endpunkte, die jeweils eine einzelne einbettbare Funktion abbilden: Buchungsseiten, Formulare, Newsletter-Anmeldung und das Chat-Widget. Jeder Endpunkt ist auf die Daten des über workspace/kundeId referenzierten Kunden beschränkt (Tenant-Isolation, siehe 25-security.md) — es gibt keine globale Auflistung fremder Daten.

Basis-URL in allen Beispielen: https://app.pronto.example (durch die eigene PRONTO-Domain ersetzen).


Buchungsseiten

GET /api/public/kalender/info

Lädt Name, Dauer, Ort und Branding einer öffentlichen Buchungsseite.

  • Methode: GET
  • Authentication: keine (öffentlich)
  • Query-Parameter: workspace (Kunden-ID, Pflicht), calendar (Slug, Pflicht)
  • Response 200:
    {
      "status": "ok",
      "kalender": {
        "id": "…", "name": "Erstgespräch", "beschreibung": "…",
        "dauerMinuten": 30, "ortTyp": "google_meet", "ortWert": null,
        "ortOptionen": [{ "typ": "google_meet", "wert": null }],
        "zeitzone": "Europe/Berlin", "farbe": "#4f46e5"
      },
      "firmenname": "Musterfirma GmbH"
    }
    
  • Fehler: 400 fehlende Parameter · 404 { "status": "nicht_gefunden" }

cURL

curl "https://app.pronto.example/api/public/kalender/info?workspace=WORKSPACE_ID&calendar=erstgespraech"

JavaScript

const res = await fetch(
  "https://app.pronto.example/api/public/kalender/info?workspace=WORKSPACE_ID&calendar=erstgespraech",
);
const data = await res.json();

PHP

<?php
$url = "https://app.pronto.example/api/public/kalender/info?workspace=WORKSPACE_ID&calendar=erstgespraech";
$data = json_decode(file_get_contents($url), true);

Python

import requests

r = requests.get(
    "https://app.pronto.example/api/public/kalender/info",
    params={"workspace": "WORKSPACE_ID", "calendar": "erstgespraech"},
)
data = r.json()

GET /api/public/kalender/slots

Freie Zeitfenster in einem Zeitraum.

  • Methode: GET
  • Authentication: keine
  • Query-Parameter: kalenderId (Pflicht, aus info), von/bis (ISO-Datum, Pflicht)
  • Response 200: { "status": "ok", "slots": ["2026-09-20T09:00:00Z", "…"] }
  • Fehler: 400 fehlende Parameter · 500 bei internem Fehler

cURL

curl "https://app.pronto.example/api/public/kalender/slots?kalenderId=KAL_ID&von=2026-09-20&bis=2026-09-27"

JavaScript

const res = await fetch(
  `https://app.pronto.example/api/public/kalender/slots?kalenderId=KAL_ID&von=2026-09-20&bis=2026-09-27`,
);
const { slots } = await res.json();

PHP

<?php
$url = "https://app.pronto.example/api/public/kalender/slots?" . http_build_query([
    "kalenderId" => "KAL_ID", "von" => "2026-09-20", "bis" => "2026-09-27",
]);
$slots = json_decode(file_get_contents($url), true)["slots"];

Python

r = requests.get(
    "https://app.pronto.example/api/public/kalender/slots",
    params={"kalenderId": "KAL_ID", "von": "2026-09-20", "bis": "2026-09-27"},
)
slots = r.json()["slots"]

POST /api/public/kalender/buchen

Bucht einen Termin.

  • Methode: POST
  • Authentication: keine
  • Request-Body (JSON):
    {
      "kundeId": "WORKSPACE_ID", "kalenderId": "KAL_ID",
      "startISO": "2026-09-20T09:00:00Z",
      "vorname": "Max", "nachname": "Mustermann",
      "email": "max@example.com", "telefon": "+49 151 00000000",
      "nachricht": "Optional", "besucherZeitzone": "Europe/Berlin",
      "ortTyp": "google_meet"
    }
    
    Pflichtfelder: kundeId, kalenderId, startISO, email, vorname.
  • Response 200: { "status": "ok", … } (Termin- und ggf. Meeting-Link-Details)
  • Fehler: 400 Pflichtfelder fehlen/ungültige Anfrage · 409 { "status": "belegt" } Slot bereits vergeben

cURL

curl -X POST "https://app.pronto.example/api/public/kalender/buchen" \
  -H "Content-Type: application/json" \
  -d '{"kundeId":"WORKSPACE_ID","kalenderId":"KAL_ID","startISO":"2026-09-20T09:00:00Z","vorname":"Max","email":"max@example.com"}'

JavaScript

const res = await fetch("https://app.pronto.example/api/public/kalender/buchen", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    kundeId: "WORKSPACE_ID",
    kalenderId: "KAL_ID",
    startISO: "2026-09-20T09:00:00Z",
    vorname: "Max",
    email: "max@example.com",
  }),
});
const ergebnis = await res.json();

PHP

<?php
$payload = json_encode([
    "kundeId" => "WORKSPACE_ID", "kalenderId" => "KAL_ID",
    "startISO" => "2026-09-20T09:00:00Z", "vorname" => "Max", "email" => "max@example.com",
]);
$ch = curl_init("https://app.pronto.example/api/public/kalender/buchen");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_RETURNTRANSFER => true,
]);
$ergebnis = json_decode(curl_exec($ch), true);

Python

r = requests.post(
    "https://app.pronto.example/api/public/kalender/buchen",
    json={
        "kundeId": "WORKSPACE_ID",
        "kalenderId": "KAL_ID",
        "startISO": "2026-09-20T09:00:00Z",
        "vorname": "Max",
        "email": "max@example.com",
    },
)
ergebnis = r.json()

GET /api/public/kalender/embed.js

Liefert das einbettbare Buchungsseiten-Skript (siehe 09-buchungsseiten.md für die Einbindung per <script>-Tag). Keine eigene Aufruf-Logik nötig — das Skript ruft info/slots/buchen selbst auf.


Formulare

GET /api/public/formulare/info

Lädt die Struktur (Seiten, Felder, Design) eines veröffentlichten Formulars.

  • Methode: GET · Authentication: keine
  • Query-Parameter: workspace (Pflicht), form (Formular-ID, Pflicht)
  • Response 200: { "status": "ok", "formular": { "id": "…", "name": "…", "seiten": […], "design": {…} } }
  • Fehler: 400 fehlende Parameter · 404 { "status": "nicht_gefunden" }

cURL

curl "https://app.pronto.example/api/public/formulare/info?workspace=WORKSPACE_ID&form=FORM_ID"

JavaScript

const { formular } = await fetch(
  `https://app.pronto.example/api/public/formulare/info?workspace=WORKSPACE_ID&form=FORM_ID`,
).then((r) => r.json());

PHP

<?php
$url = "https://app.pronto.example/api/public/formulare/info?workspace=WORKSPACE_ID&form=FORM_ID";
$formular = json_decode(file_get_contents($url), true)["formular"];

Python

formular = requests.get(
    "https://app.pronto.example/api/public/formulare/info",
    params={"workspace": "WORKSPACE_ID", "form": "FORM_ID"},
).json()["formular"]

POST /api/public/formulare/submit

Sendet eine Formular-Einsendung. Der Body ist multipart/form-data (nicht JSON), damit Datei-Uploads möglich sind.

  • Methode: POST · Authentication: keine
  • Form-Felder:
    • workspace (Pflicht), formularId (Pflicht)
    • werte — JSON-String der Feldwerte, z. B. {"feld_email":"max@example.com"}
    • datenschutzAkzeptiert"true"/"false"
    • marketingOptin"true"/"false"
    • website — Honeypot-Feld, immer leer lassen (siehe 24-rate-limits.md)
    • ladezeitMs — Millisekunden seit Formularanzeige (Bot-Schutz)
    • datei:<feldId> — optionale Datei-Uploads, ein Eintrag pro Datei-Feld
  • Response 200: { "status": "ok", "erfolg": { "typ": "nachricht", "titel": "Danke!", … } }
  • Fehler: 400 fehlende Pflichtfelder/ungültige Anfrage · 429 echtes Rate-Limit erreicht (Spam wird bewusst mit 200 beantwortet, siehe 24-rate-limits.md)

cURL

curl -X POST "https://app.pronto.example/api/public/formulare/submit" \
  -F "workspace=WORKSPACE_ID" \
  -F "formularId=FORM_ID" \
  -F 'werte={"feld_email":"max@example.com","feld_name":"Max Mustermann"}' \
  -F "datenschutzAkzeptiert=true" \
  -F "website=" \
  -F "ladezeitMs=4200"

JavaScript

const form = new FormData();
form.append("workspace", "WORKSPACE_ID");
form.append("formularId", "FORM_ID");
form.append("werte", JSON.stringify({ feld_email: "max@example.com", feld_name: "Max Mustermann" }));
form.append("datenschutzAkzeptiert", "true");
form.append("website", ""); // Honeypot — leer lassen
form.append("ladezeitMs", String(Date.now() - formularAngezeigtAb));

const res = await fetch("https://app.pronto.example/api/public/formulare/submit", {
  method: "POST",
  body: form,
});
const ergebnis = await res.json();

PHP

<?php
$ch = curl_init("https://app.pronto.example/api/public/formulare/submit");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS => [
        "workspace" => "WORKSPACE_ID",
        "formularId" => "FORM_ID",
        "werte" => json_encode(["feld_email" => "max@example.com", "feld_name" => "Max Mustermann"]),
        "datenschutzAkzeptiert" => "true",
        "website" => "",
        "ladezeitMs" => "4200",
    ],
]);
$ergebnis = json_decode(curl_exec($ch), true);

Python

r = requests.post(
    "https://app.pronto.example/api/public/formulare/submit",
    data={
        "workspace": "WORKSPACE_ID",
        "formularId": "FORM_ID",
        "werte": '{"feld_email":"max@example.com","feld_name":"Max Mustermann"}',
        "datenschutzAkzeptiert": "true",
        "website": "",
        "ladezeitMs": "4200",
    },
)
ergebnis = r.json()

GET /api/public/formulare/embed.js

Liefert das einbettbare Formular-Skript (siehe 10-formulare.md).


Newsletter

POST /api/public/newsletter/signup

  • Methode: POST · Authentication: keine
  • Request-Body (JSON):
    {
      "kundeId": "WORKSPACE_ID", "email": "max@example.com", "vorname": "Max",
      "quelle": "landingpage", "website": "", "ladezeitMs": 4200,
      "datenschutzAkzeptiert": true
    }
    
    datenschutzAkzeptiert: true ist serverseitig zwingend erforderlich.
  • Response 200: { "status": "ok" } oder { "status": "bestaetigung_erforderlich" } (Double-Opt-in-Mail wurde verschickt)
  • Fehler: 400 fehlende Einwilligung/ungültige Anfrage · 429 Rate-Limit

cURL

curl -X POST "https://app.pronto.example/api/public/newsletter/signup" \
  -H "Content-Type: application/json" \
  -d '{"kundeId":"WORKSPACE_ID","email":"max@example.com","vorname":"Max","datenschutzAkzeptiert":true}'

JavaScript

const res = await fetch("https://app.pronto.example/api/public/newsletter/signup", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    kundeId: "WORKSPACE_ID",
    email: "max@example.com",
    vorname: "Max",
    datenschutzAkzeptiert: true,
  }),
});
const ergebnis = await res.json();

PHP

<?php
$payload = json_encode([
    "kundeId" => "WORKSPACE_ID", "email" => "max@example.com",
    "vorname" => "Max", "datenschutzAkzeptiert" => true,
]);
$ch = curl_init("https://app.pronto.example/api/public/newsletter/signup");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_RETURNTRANSFER => true,
]);
$ergebnis = json_decode(curl_exec($ch), true);

Python

r = requests.post(
    "https://app.pronto.example/api/public/newsletter/signup",
    json={
        "kundeId": "WORKSPACE_ID",
        "email": "max@example.com",
        "vorname": "Max",
        "datenschutzAkzeptiert": True,
    },
)
ergebnis = r.json()

GET /api/public/newsletter/confirm

Double-Opt-in-Bestätigungslink — wird ausschließlich aus der Anmelde-E-Mail heraus aufgerufen (?token=…), liefert eine fertige HTML-Bestätigungsseite. Nicht für eigene Integrationen gedacht.

GET /api/public/newsletter/unsubscribe

Abmeldelink, automatisch in jeder Newsletter-Mail (?rid=…), liefert eine fertige HTML-Seite. Ebenfalls nicht für eigene Integrationen gedacht.


Chat-Widget

GET /api/public/widget.js

Liefert das einbettbare Chat-Widget-Skript. Einbindung:

<script src="https://app.pronto.example/api/public/widget.js" data-pronto="WIDGET_KEY"></script>

GET /api/public/widget/config

Lädt die öffentliche Anzeigekonfiguration (Farbe, Modus, Aktiv-Status) für einen Widget-Key. CORS-fähig (OPTIONS-Preflight unterstützt).

  • Query-Parameter: key (Pflicht)
  • Response 200: { "aktiv": true, … } — bei inaktivem Widget, falscher Domain oder aufgebrauchtem Credit-Guthaben immer { "aktiv": false } (kein Fehlercode, damit das Widget auf der Kundenseite einfach unsichtbar bleibt).

POST /api/public/widget/message

Sendet eine Chat-Nachricht.

  • Request-Body (JSON): { "key": "WIDGET_KEY", "session_id": "…", "text": "…", "typ": "text" } (typ: "text" | "button" | "kontakt")
  • Fehler: 400 ungültige Anfrage · 403 Domain nicht freigegeben · 404 Widget inaktiv/nicht gefunden

POST /api/public/widget/termin

Rückmeldung nach einer im Widget gebuchten Terminanfrage (event_uri, name, email, telefon).

Diese drei Widget-Endpunkte werden ausschließlich vom mitgelieferten widget.js-Skript selbst aufgerufen — eine eigene Implementierung dagegen ist möglich, aber nicht der vorgesehene Weg (das Skript einbinden reicht).


Nicht Teil dieser Referenz

api/public/pronto-ai/chat (Bearer-Token-authentifiziert, nur für eingeloggte PRONTO-Nutzer, aktuell deaktiviert), alle api/public/*/webhook und */run-Endpunkte (interne Callbacks, siehe 19-webhooks.md) sowie oauth/*/callback (OAuth-Redirect-Ziele) sind keine für externe Integrationen vorgesehenen Endpunkte.