ACP · Integration

IntegrationshandbuchIntegration manual

Schritt für Schritt zur AnbindungStep-by-step integration manual

Acht Abschnitte, in der Reihenfolge, in der sie zu tun sind. Jeder Befehl und jeder Endpunkt auf dieser Seite ist gegen dieses Repository ausgeführt worden; die gezeigten Antworten sind echte Antworten, keine Beispiele aus dem Kopf. Eight sections, in the order you do them. Every command and every endpoint on this page was run against this repository; the responses shown are real responses, not examples from memory.

Voraussetzung für die Abschnitte 1 bis 2: Python 3.11 oder neuer, git, für den Container zusätzlich Docker. Zugangsdaten brauchen Sie bis Abschnitt 8 keine. Prerequisites for sections 1 and 2: Python 3.11 or newer, git, and Docker for the container. You need no credentials anywhere up to section 8.

1

Ohne ZugangsdatenZero credentials

Lokal auf Mocks laufen lassenRun it locally on mocks

Vier Befehle. Der erste baut die Umgebung, der zweite ist der QA-Gate, der dritte spielt ein vollständiges Gespräch durch, der vierte startet die Betriebskonsole. Four commands. The first builds the environment, the second is the QA gate, the third plays a complete conversation, the fourth starts the operations console.

make install
make gate
make demo
make console

Was der Gate tatsächlich ausführt — nachlesbar mit make -n gate: What the gate actually runs — readable with make -n gate:

.venv/bin/ruff check src tests
.venv/bin/mypy
.venv/bin/pytest -m "not live"
bash scripts/check_isolation.sh

Die Konsole zeigt ihre Oberfläche öffentlich, holt aber jede Zahl authentifiziert. Ohne Anmeldung erscheint deshalb der Hinweis „Sitzung abgelaufen“. Für den lokalen Blick genügt ACP_ALLOW_ANON=1 — in live verweigert der Start damit den Dienst, absichtlich. The console serves its shell publicly but fetches every number authenticated. Without a sign-in you therefore see the “session expired” banner. For a local look, ACP_ALLOW_ANON=1 is enough — in live that same variable refuses the boot, deliberately.

ACP_ENV=mock ACP_ALLOW_ANON=1 .venv/bin/uvicorn acp.console.app:app --host 127.0.0.1 --port 8080

Weitere Ziele: make test (nur Tests), make test-cov (mit Abdeckung), make lint, make typecheck, make audit (Abhängigkeitsprüfung gegen bekannte Schwachstellen). Further targets: make test (tests only), make test-cov (with coverage), make lint, make typecheck, make audit (dependency scan against known vulnerabilities).

2

Ohne ZugangsdatenZero credentials

Im ContainerIn a container

make docker-build
make compose-up

Der Stapel kommt ohne jedes Geheimnis hoch: ACP_ENV steht per Vorgabe auf mock, und ../.env wird eingebunden, falls vorhanden, aber nicht verlangt. Genau das ist der Punkt — ein Mandant wird live, indem er Geheimnisse liefert, nicht indem jemand Code ändert. The stack comes up with no secret at all: ACP_ENV defaults to mock, and ../.env is included if present but never required. That is the point — a tenant goes live by supplying secrets, not by anyone changing code.

Prüfen Sie den Container so: Check the container like this:

curl -s http://127.0.0.1:8080/health
{"status":"ok","version":"0.1.0","region":"eu-central-1"}
3

KonfigurationConfiguration

Einen Mandanten konfigurierenConfigure a tenant

Zwei Geltungsbereiche, bewusst getrennt: Plattform gilt für das ganze Deployment und wird einmal beim Start gelesen; Mandant gilt pro Kunde und kommt später aus dem Mandantenregister. Nichts verändert os.environ — viele Mandanten leben mit eigener Konfiguration im selben Prozess. Two scopes, deliberately separated: platform applies to the whole deployment and is read once at boot; tenant applies per customer and later comes from the tenant registry. Nothing mutates os.environ — many tenants coexist in one process with their own configuration.

VariableVariable BereichScope BedeutungMeaning
ACP_ENV Plattformplatform Nur mock, test und ci sind offline. Alles andere gilt als live.Only mock, test and ci are offline. Everything else counts as live.
ACP_CONSOLE_SECRET Plattformplatform Signierschlüssel der Konsolensitzungen, mindestens 32 Byte. In live Pflicht.Signing key for console sessions, at least 32 bytes. Mandatory in live.
ACP_INGEST_TOKEN Plattformplatform Token der Sprachworker-Brücke. Nicht gesetzt heißt geschlossen (503), nicht offen.Token for the voice-worker bridge. Unset means closed (503), not open.
ACP_METRICS_TOKEN Plattformplatform Token für den Prometheus-Abruf. Ebenfalls fail-closed.Token for the Prometheus scrape. Fail-closed as well.
ACP_CONSOLE_USERS Plattformplatform Pfad zu console_users.json mit argon2id-Hashes.Path to console_users.json holding argon2id hashes.
ACP_ALLOW_ANON Plattformplatform Unangemeldete Konsole. Nur lokal; in live verweigert der Start.Unauthenticated console. Local only; in live the boot refuses.
ACP_DATA_ROOT Plattformplatform Wurzel je Mandant: <root>/<tenant>/…. Vorgabe var/tenants.Root per tenant: <root>/<tenant>/…. Defaults to var/tenants.
ACP_TENANT_ID Mandanttenant DNS-taugliche Kennung. Traversal ist damit nicht darstellbar, nicht bloß gefiltert.A DNS-safe slug. Traversal is unrepresentable, not merely filtered.
ACP_REGION Mandanttenant Datenhaltung, Vorgabe eu-central-1.Data residency, defaults to eu-central-1.
ACP_APPROVAL_EUR Mandanttenant Betrag, ab dem eine Aktion eine menschliche Freigabe braucht.The amount above which an action needs a human approval.
ACP_CONFIDENTIAL_FRONTIER Mandanttenant Dürfen vertrauliche Inhalte an ein Frontier-Modell? Vorgabe: nein.May confidential traffic reach a frontier model? Defaults to no.
ACP_STT · ACP_TTS · ACP_TRANSPORT Mandanttenant Engine-Wahl. Auf mock bis die Zugangsdaten da sind; jeder Pfad kippt einzeln.Engine selection. On mock until credentials arrive; each path flips on its own.
ACP_VOICE_MODE · ACP_TURN_BUDGET_MS Mandanttenant Kaskadiert oder Sprache-zu-Sprache, und das Latenzbudget je Zug (Vorgabe 800 ms).Cascaded or speech-to-speech, and the per-turn latency budget (800 ms by default).
ACP_WEBHOOK_ALLOW_HOSTS Plattformplatform Einzige Ausnahme der Egress-Richtlinie: exakte Hostnamen, kommagetrennt.The only exception to the egress policy: exact hostnames, comma-separated.

Jedes Geheimnis darf stattdessen aus einer Datei kommen. <NAME>_FILE gewinnt gegen <NAME>, und der Wert taucht damit weder in docker inspect noch in einem Absturzbericht auf. Leer gilt als nicht gesetzt: Every secret may come from a file instead. <NAME>_FILE beats <NAME>, so the value appears neither in docker inspect nor in a crash dump. Blank counts as unset:

ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_api_key
ACP_CONSOLE_SECRET_FILE=/run/secrets/acp_console_secret
ACP_INGEST_TOKEN_FILE=/run/secrets/acp_ingest_token

Und so sieht die fail-closed-Regel aus, wenn etwas fehlt. Beachten Sie den zweiten Fall: production ist kein Sonderwort, sondern schlicht „nicht offline“ — deshalb greift dieselbe Prüfung: And this is what the fail-closed rule looks like when something is missing. Note the second case: production is not a special word, merely “not offline” — so the same check applies:

ACP_ENV=production .venv/bin/uvicorn acp.console.app:app

SecretError: refusing to start: ACP_ENV='production' is treated as a LIVE
deployment (only ['ci', 'mock', 'test'] are offline).
  - required secret ACP_CONSOLE_SECRET is not set
    (set ACP_CONSOLE_SECRET or ACP_CONSOLE_SECRET_FILE). Refusing to start.
  - required secret ACP_INGEST_TOKEN is not set
    (set ACP_INGEST_TOKEN or ACP_INGEST_TOKEN_FILE). Refusing to start.

Alle Probleme werden auf einmal gemeldet, nicht eins nach dem anderen — und nicht erst bei der ersten Anfrage, die zufällig ein Geheimnis braucht. Every problem is reported at once rather than one at a time — and at startup, not on the first request that happens to need a secret.

4

APIAPI

Der erste API-AufrufThe first API call

GET /v1/health ist der einzige unauthentifizierte Endpunkt der Produkt-API. Alles andere braucht einen Mandantenschlüssel als Bearer-Token. GET /v1/health is the only unauthenticated endpoint in the product API. Everything else needs a tenant key as a bearer token.

curl -s http://127.0.0.1:8080/v1/health
{"status":"ok","api_version":"v1"}

Es gibt bewusst keinen Ausstellungs-Endpunkt: Ausstellen vergibt einen mandantenweiten Zugang, und außerhalb der HTTP-Oberfläche kann eine übernommene Konsolensitzung keinen Schlüssel erzeugen. Ausgestellt wird auf dem Host; der Klartext erscheint genau einmal, gespeichert wird nur sein Hash: There is deliberately no issuance endpoint: issuing hands out a tenant-wide credential, and keeping it off the HTTP surface means a hijacked console session cannot mint one. Issuance happens on the host; the plaintext appears exactly once and only its hash is stored:

# On a deployed instance. The key file sits on the data volume, so the key
# keeps working across restarts and releases.
docker exec -i acp-console-1 python -m acp.api issue-api acme read,write "acme prod"

# Locally, against a checkout:
ACP_KEYS_DIR=var python -m acp.api issue-api acme read,write "local dev"

# List what exists, and take one back. Revocation is refused from the next
# request onward — no restart, because the file decides authority.
python -m acp.api list acme
python -m acp.api revoke <key-id>
.venv/bin/uvicorn acp.console.app:app --host 127.0.0.1 --port 8080

Der Schlüssel hat die Form acp_sk_<key-id>_<secret>. Gespeichert wird nur sein Hash; der Klartext erscheint genau einmal. Damit fragen Sie, wer Sie sind: The key has the shape acp_sk_<key-id>_<secret>. Only its hash is stored; the plaintext appears exactly once. With it you ask who you are:

curl -s http://127.0.0.1:8080/v1/tenant \
     -H "Authorization: Bearer acp_sk_0f521c65f209934b_<secret>"

{"tenant_id":"acme","key_id":"0f521c65f209934b","scopes":["read","write"],
 "region":"eu-central-1"}

Der Rest der Oberfläche, wie er in src/acp/api/v1.py steht: The rest of the surface, as it stands in src/acp/api/v1.py:

EndpunktEndpoint BereichScope ZweckPurpose
GET /v1/health öffentlichpublic Lebenszeichen für LastverteilerLiveness for load balancers
GET /v1/tenant read Zu welchem Mandanten der Schlüssel gehörtWhich tenant this key belongs to
GET /v1/conversations read Gespräche dieses MandantenThis tenant's conversations
POST /v1/conversations/{id}/messages write Eine Nachricht senden, die Antwort erhaltenSend a message, receive the reply
GET · POST /v1/knowledge read · write Wissen lesen, Entwurf anlegenRead knowledge, create a draft
GET /v1/knowledge-search read Suche über veröffentlichtes, kundentaugliches WissenRetrieval over published, customer-safe knowledge
GET · POST /v1/webhooks read · write Endpunkte auflisten und registrierenList and register endpoints
GET /v1/events read Alle abonnierbaren EreignistypenEvery subscribable event type
WS /v1/conversations/{id}/stream read Live-Strom der Wortbeiträge; Schlüssel als Abfrageparameter, weil Browser bei einem WebSocket keine Kopfzeilen setzen könnenLive turn stream; the key travels as a query parameter because browsers cannot set headers on a WebSocket handshake
5

WissenKnowledge

Einen Entwurf anlegen — und warum nur einen EntwurfCreate a draft — and why only a draft

curl -s -X POST http://127.0.0.1:8080/v1/knowledge \
     -H "Authorization: Bearer acp_sk_<key-id>_<secret>" \
     -H "Content-Type: application/json" \
     -d '{"title":"Versandkosten",
          "body":"Der Versand innerhalb Deutschlands kostet 4,90 EUR.",
          "category":"service","language":"de","summary":"Versandkosten Inland"}'

{"record_id":"versandkosten","title":"Versandkosten",
 "summary":"Versandkosten Inland","category":"service","language":"de",
 "status":"draft","authority":"internal","audience":"internal",
 "version":1,"citable":false}

Beachten Sie "status":"draft" und "citable":false. Ein Entwurf steuert keinen Agenten: nur veröffentlichte Einträge erreichen die Produktion, und nur eine amtliche Quelle wird gegenüber Kunden zitierfähig. Note "status":"draft" and "citable":false. A draft steers no agent: only published records reach production, and only an official source becomes citable to customers.

Der Lebenszyklus ist im Code erzwungen, nicht bloß dokumentiert: The lifecycle is enforced in code, not merely documented:

draft ──→ verified ──→ published ──→ retired
  ↑            │             │
  └────────────┴─────────────┘

SchemaError: illegal transition draft -> published for 'versandkosten'

Ehrlich zum heutigen Stand: die Freigabe ist eine menschliche Handlung, aber sie hat noch keinen HTTP-Endpunkt. Verifizieren und Veröffentlichen laufen serverseitig über die Registry — registry.publish(record_id, actor="…"), was den Commit schreibt und den Freigebenden stempelt. Die Konsole zeigt Bestand, Status und Herkunft; sie täuscht keine Schaltfläche vor, die es nicht gibt. Honest about today: approval is a human action, but it has no HTTP endpoint yet. Verify and publish run server-side through the registry — registry.publish(record_id, actor="…"), which writes the commit and stamps the approver. The console shows the inventory, status and provenance; it fakes no button that does not exist.

Was der Agent später findet, prüfen Sie so — die Suche liefert nur veröffentlichtes und kundentaugliches Material: Check what the agent will find like this — retrieval returns only published, customer-safe material:

curl -s -G http://127.0.0.1:8080/v1/knowledge-search \
     -H "Authorization: Bearer acp_sk_<key-id>_<secret>" \
     --data-urlencode "q=Was kostet der Versand?" --data-urlencode "language=de"
6

EreignisseEvents

Webhooks registrieren und prüfenRegister and verify webhooks

curl -s -X POST http://127.0.0.1:8080/v1/webhooks \
     -H "Authorization: Bearer acp_sk_<key-id>_<secret>" \
     -H "Content-Type: application/json" \
     -d '{"url":"https://example.com/acp/webhook",
          "events":["turn.completed","conversation.escalated"]}'

{"endpoint_id":"ccd086565d831d82","url":"https://example.com/acp/webhook",
 "events":["turn.completed","conversation.escalated"],"active":true,
 "secret":"BYHpdmnTHljmXPPnEG4nH8Xbukk-CCrLSeXuO-Lr7ig"}

Das Signaturgeheimnis erscheint wie ein Schlüssel genau einmal, hier in der Antwort auf die Registrierung. Ein späteres GET /v1/webhooks liefert an dieser Stelle null. Abonnierbar sind: conversation.started, conversation.ended, turn.completed, approval.requested, conversation.escalated, turn.budget_exceeded. Eine leere Liste heißt „alles“. The signing secret appears exactly once, like a key — here, in the registration response. A later GET /v1/webhooks returns null in that field. Subscribable events: conversation.started, conversation.ended, turn.completed, approval.requested, conversation.escalated, turn.budget_exceeded. An empty list means “everything”.

So sieht eine Zustellung aus. Der Rumpf wird mit sortierten Schlüsseln und ohne Leerzeichen serialisiert, damit die Signatur reproduzierbar ist: This is what a delivery looks like. The body is serialised with sorted keys and no whitespace so the signature is reproducible:

X-ACP-Event:     turn.completed
X-ACP-Signature: t=1787558400,v1=f442c3f4a3a99fa506d0ca559c080ffcd242b7c3a0363508176533f425c6f1e8

{"data":{"conversation_id":"web-1a2b3c4d5e6f7a8b"},"event":"turn.completed",
 "occurred_at":1787558400.0,"tenant_id":"acme"}

v1 = HMAC-SHA256( secret, "{timestamp}.{body}" )

Der Zeitstempel liegt im signierten Material — eine mitgeschnittene Zustellung lässt sich deshalb nicht später erneut abspielen, sofern Sie die Frische prüfen. Lehnen Sie alles ab, was älter als 300 Sekunden ist, und signieren Sie gegen den Rumpf wie empfangen, nie gegen ein neu serialisiertes Objekt. The timestamp sits inside the signed material — a captured delivery therefore cannot be replayed later, provided you check freshness. Reject anything older than 300 seconds, and verify against the body as received, never against a re-serialised object.

Prüfung in Python:Verification in Python:

import hashlib, hmac, time

def verify(secret: str, body: str, header: str, tolerance_s: int = 300) -> bool:
    try:
        parts = dict(p.split("=", 1) for p in header.split(","))
        timestamp, received = int(parts["t"]), parts["v1"]
    except (ValueError, KeyError):
        return False
    if abs(time.time() - timestamp) > tolerance_s:
        return False
    expected = hmac.new(
        secret.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, received)

Dasselbe in JavaScript (Node):The same in JavaScript (Node):

const crypto = require("node:crypto");

function verify(secret, body, header, toleranceS = 300) {
  const parts = {};
  for (const piece of header.split(",")) {
    const i = piece.indexOf("=");
    parts[piece.slice(0, i)] = piece.slice(i + 1);
  }
  const timestamp = Number(parts.t);
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceS) return false;
  const expected = crypto.createHmac("sha256", secret)
    .update(`${timestamp}.${body}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Beide Fassungen vergleichen in konstanter Zeit. Wer die Plattform selbst betreibt, kann stattdessen acp.api.webhooks.verify_signature aufrufen — dieselbe Implementierung, die auch signiert. Both versions compare in constant time. If you run the platform yourself you can call acp.api.webhooks.verify_signature instead — the same implementation that signs.

Eine private Adresse wird abgelehnt. Das ist die Funktion, nicht der Fehler: Ihre URL ist ein Ziel, das die Plattform aus ihrer eigenen Vertrauenszone heraus anspricht. A private address is refused. That is the feature, not the fault: your URL is a destination the platform will reach from inside its own trust boundary.

HTTP/1.1 400 Bad Request

{"detail":"address 10.0.0.5 is not publicly routable. Policy: https only;
 no loopback, private, link-local, multicast or reserved addresses;
 no *.internal / *.local / localhost; no known internal service ports;
 DNS re-checked at delivery time (rebinding);
 override with ACP_WEBHOOK_ALLOW_HOSTS"}

Der erste Satz wechselt mit der verletzten Regel — etwa webhook url must use https oder host 'hooks.internal' is internal. Geprüft wird bei der Registrierung und erneut bei jeder Zustellung, weil DNS angreiferkontrolliert ist. Zustellung selbst ist „fire and forget“ mit bis zu drei Versuchen; 4xx außer 429 wird nicht wiederholt, und ein hängender Empfänger kann ein laufendes Gespräch weder verzögern noch scheitern lassen. The first sentence changes with the rule that was broken — for example webhook url must use https or host 'hooks.internal' is internal. It is checked at registration and again at every delivery, because DNS is attacker-controlled. Delivery itself is fire-and-forget with up to three attempts; 4xx other than 429 is not retried, and a hanging receiver can neither delay nor fail a live conversation.

7

WebsiteWebsite

Das Chat-Widget einbettenEmbed the chat widget

Ein Script-Tag, keine Abhängigkeit, kein Build-Schritt: One script tag, no dependency, no build step:

<script src="https://acp.example.eu/static/widget/acp-widget.js"
        data-key="acp_pk_<key-id>_<secret>"
        data-lang="de"
        async></script>

Den Wortlaut können Sie sich vorher ansehen: You can preview the exact wording:

curl -s http://127.0.0.1:8080/widget/v1/disclosure

{"de":"Sie sprechen mit einem KI-Assistenten. Auf Wunsch übergebe ich an einen Menschen.",
 "en":"You are speaking with an AI assistant. I can hand over to a person at any time."}

Das Widget rendert in einem Shadow-DOM: Ihr Seiten-CSS kann es nicht kaputt machen, und es kann Ihre Seite nicht umgestalten. Farben kommen aus --acp-* und vererben durch die Shadow-Grenze — Ihre Marke ist ein Token-Override, kein Fork. Modellausgabe wird über textContent gesetzt, nie über innerHTML. Mit data-lang="en" startet es englisch, mit data-title tragen Sie eine eigene Überschrift ein. The widget renders in a shadow DOM: your page CSS cannot break it, and it cannot restyle your page. Colours come from --acp-* and inherit through the shadow boundary — your brand is a token override, not a fork. Model output is set through textContent, never innerHTML. With data-lang="en" it starts in English, and data-title sets your own heading.

8

FehlersucheTroubleshooting

Was schiefgeht — und warum es so gedacht istWhat goes wrong — and why it is meant to

Die meisten dieser Antworten sind kein Defekt, sondern eine Entscheidung. Alle Texte unten sind echte Antworten dieses Repositories. Most of these answers are a decision, not a defect. Every message below is a real response from this repository.

Sie sehenYou see UrsacheCause AbhilfeRemedy
401 · invalid API key
/v1/*
Kopfzeile fehlt, falsch geformt, Schlüssel unbekannt oder widerrufen — oder ein acp_pk_ als Bearer-Token benutzt.Header missing, malformed, key unknown or revoked — or an acp_pk_ used as a bearer token. Authorization: Bearer acp_sk_<key-id>_<secret> senden. Alle vier Fälle antworten absichtlich gleich.Send Authorization: Bearer acp_sk_<key-id>_<secret>. All four cases answer identically on purpose.
403 · key lacks the 'write' scope Schreibender Aufruf mit einem Nur-Lese-Schlüssel.A writing call with a read-only key. Schlüssel mit write verwenden — oder den Aufruf auf Lesen beschränken.Use a key with write — or keep the call read-only.
404 · conversation not found Das Gespräch gehört einem anderen Mandanten oder existiert nicht.The conversation belongs to another tenant, or does not exist. Kein Fehler: 403 würde bestätigen, dass es existiert. Kennung und Mandant prüfen.Not a bug: 403 would confirm it exists. Check the id and the tenant.
401 · invalid embed key or origin
/widget/v1/session
Die Herkunft steht nicht auf der Liste des Schlüssels, oder es kam gar keine Origin-Kopfzeile.The origin is not on the key's allow-list, or no Origin header arrived at all. Herkunft exakt eintragen lassen, inklusive Schema und Port. Aus curl ohne Origin scheitert es immer.Have the origin registered exactly, including scheme and port. From curl without an origin it always fails.
503 · metrics endpoint is not configured ACP_METRICS_TOKEN ist nicht gesetzt.ACP_METRICS_TOKEN is not set. Token setzen und als X-ACP-Token senden. Unkonfiguriert wird geschlossen, nicht veröffentlicht.Set the token and send it as X-ACP-Token. Unconfigured closes the endpoint rather than publishing it.
503 · ingest bridge is not configured ACP_INGEST_TOKEN ist nicht gesetzt — die Sprachworker-Brücke bleibt zu.ACP_INGEST_TOKEN is not set — the voice-worker bridge stays shut. Token setzen. Früher hat ein leerer Token die Prüfung abgeschaltet; genau das war Prüfbefund B3.Set the token. An empty token used to disable the check; that was exactly audit blocker B3.
503 · no agent brain is configured Die Anwendung läuft ohne Agenten, etwa als reine API-Instanz.The app runs without an agent, for example as a bare API instance. Beim Bau create_app(brain=…) mitgeben. Ohne Agenten wird keine Antwort erfunden.Pass create_app(brain=…) when building. Without an agent no reply is invented.
400 · Policy: https only; … Die Webhook-URL verstößt gegen die Egress-Richtlinie: http, private Adresse, interner Name oder interner Dienstport.The webhook URL breaks the egress policy: http, a private address, an internal name or an internal service port. Öffentlich erreichbare https-URL nehmen. Für echte Ausnahmen ACP_WEBHOOK_ALLOW_HOSTS, opt-in.Use a publicly reachable https URL. For genuine exceptions there is ACP_WEBHOOK_ALLOW_HOSTS, opt-in.
409 · an identical endpoint is already registered Gleiche URL und gleiche Ereignisliste zweimal registriert; pro Mandant sind höchstens 20 Endpunkte erlaubt.The same URL with the same event list registered twice; at most 20 endpoints per tenant. Bestand mit GET /v1/webhooks prüfen, statt blind neu zu registrieren.Check the list with GET /v1/webhooks instead of blindly registering again.
SecretError · refusing to start ACP_ENV ist nicht mock, test oder ci, und ACP_CONSOLE_SECRET oder ACP_INGEST_TOKEN fehlt.ACP_ENV is not mock, test or ci, and ACP_CONSOLE_SECRET or ACP_INGEST_TOKEN is missing. Beide setzen (Signierschlüssel ≥ 32 Byte) oder bewusst auf mock bleiben. Ein Tippfehler im Wert gilt als live — das ist die sichere Richtung.Set both (signing key ≥ 32 bytes) or stay on mock deliberately. A typo in the value counts as live — that is the safe direction.
AuthError · ACP_ALLOW_ANON must never be set when ACP_ENV=live Die lokale Abkürzung für eine unangemeldete Konsole ist in eine Live-Umgebung gerutscht.The local shortcut for an unauthenticated console slipped into a live environment. Variable entfernen und Konsolenkonten hinterlegen. Nur lokal setzen, nie in einer Vorlage für Produktion.Remove the variable and configure console users. Set it locally only, never in a production template.
Konsole lädt, zeigt aber „Sitzung abgelaufen“Console loads but shows “session expired” Die Oberfläche ist öffentlich, die Daten nicht — ohne Anmeldung antwortet jede Datenroute mit 401.The shell is public, the data is not — without a sign-in every data route answers 401. Anmelden, oder für den lokalen Blick ACP_ALLOW_ANON=1 setzen.Sign in, or set ACP_ALLOW_ANON=1 for a local look.