diff --git a/docs/06-schnittstellen.md b/docs/06-schnittstellen.md
new file mode 100644
index 0000000..62c5377
--- /dev/null
+++ b/docs/06-schnittstellen.md
@@ -0,0 +1,138 @@
+# 06 Schnittstellen
+
+Zwei fremde Systeme hängen am CRM: die **ICON Website** liest die Projektbörse,
+und das **perinco CRM** tauscht Kontakte in beide Richtungen aus.
+
+## ICON Website
+
+Seit dem 14.09.2026 liegt die Website auf demselben Server wie das CRM
+(`/var/www/icon_website/dist`, Vhost `icon-experts.de` + `www`). Vorher lief sie
+auf `212.227.135.41` – derselben Maschine wie perinco CRM, n8n, Docker und
+Supabase.
+
+```mermaid
+flowchart LR
+ V(("Besucher")) --> W["Apache
icon-experts.de"]
+ W -->|"/"| S["Website (dist)"]
+ W -->|"/api/public
ProxyPass"| E["Express :5007"]
+ E --> DB[("icon_CRM")]
+```
+
+Der Vhost reicht **`/api/public` an das CRM-Backend durch**. Das ist kein
+Schönheitsfehler, sondern der Grund, warum die Content-Security-Policy der Seite
+weiter mit `connect-src 'self'` auskommt: der Aufruf ist gleicher Herkunft.
+Zeigte die Seite direkt auf `icrm.icon-experts.de`, müsste dieser Host in der CSP
+stehen und der Browser bräuchte einen CORS-Vorabruf.
+
+### Der öffentliche Endpunkt
+
+`GET /api/public/tenders` – ohne Anmeldung, nur Lesen, neueste zuerst. Dazu
+`GET /api/public/tenders/:id` für einen direkten Link. Beide antworten mit
+`Access-Control-Allow-Origin: *` und `Cache-Control: public, max-age=60`.
+
+```json
+[{ "id": 8, "title": "…", "status": "Aktiv", "notes": "
…
",
+ "created_at": "…", "updated_at": "…" }]
+```
+
+**Nicht** ausgeliefert werden Kunde, zugewiesene Experten, Quelle, Fristen und
+Phase. Das ist der eigentliche Gewinn gegenüber dem vorherigen Weg: die Website
+las mit dem Supabase-Anon-Key direkt in der Tabelle und durfte `select('*')` –
+der Schlüssel lag dafür im Browser-Bundle.
+
+> **Zwei Bedingungen, nicht eine:** `online = 1` **und** `status <> 'Archiviert'`.
+> Im Altbestand steht eine archivierte Ausschreibung, deren Schalter noch an ist –
+> über Supabase war sie unsichtbar, über diesen Endpunkt wäre sie mit der
+> Kategorie „Archiviert" auf der Website aufgetaucht.
+
+Auf Seiten der Website hängt das an `src/lib/iconCrm.js`; `src/pages/Experts.jsx`
+ruft nur noch `fetchTenders()` auf. `@supabase/supabase-js` wird dort seither
+nirgends mehr benutzt.
+
+**Der Schalter `online` in der Liste der Ausschreibungen entscheidet also
+unmittelbar, was auf der Website steht** – einen Klick entfernt, ohne Umweg.
+
+## perinco CRM, in beide Richtungen
+
+*Kontakte → Häkchen „perinco".* Die Kontakte des perinco CRM werden zusätzlich
+angezeigt – **grün umrandet**, rein lesend, und **nichts davon wird
+gespeichert**. Ein nächtlicher Abgleich in die eigene Datenbank wäre der falsche
+Weg: dann stünde derselbe Mensch in zwei Systemen, und irgendwann wäre zu
+klären, welcher Eintrag gilt.
+
+Dasselbe gibt es spiegelbildlich drüben: im perinco CRM erscheinen die
+ICON-Kontakte hinter dem Häkchen „ICON", **gold** umrandet.
+
+```mermaid
+flowchart LR
+ I["ICON CRM
icrm.icon-experts.de"] -->|"GET /api/integrations/contacts
Bearer-Token"| P["perinco CRM
pcrm.perinco.de"]
+ P -->|"GET /api/integrations/contacts
Bearer-Token"| I
+```
+
+| Richtung | Wer fragt | Wer antwortet | Token |
+| --- | --- | --- | --- |
+| ICON liest perinco | `server/src/routes/perinco.js` | drüben `server/src/routes/integrations.js` | hier `PERINCO_API_TOKEN` (+ `PERINCO_API_URL`), drüben `ICON_API_TOKEN` |
+| perinco liest ICON | drüben `server/src/routes/iconCrm.js` | hier `server/src/routes/integrations.js` | hier `PERINCO_INBOUND_TOKEN`, drüben der gleiche Wert |
+
+Mitgeschickt wird der Token als `Authorization: Bearer …`.
+
+Beide Router arbeiten mit **festem Token statt JWT** – der Aufrufer ist ein
+Backend, kein angemeldeter Mensch – und verglichen wird mit
+`crypto.timingSafeEqual`. Beide kennen ausschließlich `GET`; einen Schreibweg
+gibt es nicht, auch später nicht.
+
+Fehlt einer der Werte, bleibt das Häkchen unsichtbar und beide Systeme laufen
+normal weiter. Ein Knopf, der nur eine Fehlermeldung erzeugt, ist keiner.
+
+### Wer weitergegeben wird
+
+**Ein Häkchen am einzelnen Datensatz entscheidet.** Im perinco CRM trägt jeder
+Kontakt das Feld *„Im ICON CRM anzeigen"* (`crm_leads.icon`); nur wer es gesetzt
+hat, taucht hier auf. Beim Anlegen der Spalte wurden alle vorhandenen Kontakte
+auf gesetzt gestellt – sie waren bis dahin sichtbar, und ein neues Feld darf
+nichts verschwinden lassen. **Neue Kontakte starten ungesetzt.**
+
+Kontakte im Vorbereich „Aus Mails" (`mail_pool = 1`) bleiben außen vor:
+unvollständig und teils Dubletten.
+
+### Feldabbildung
+
+Abgebildet wird auf die **vorhandenen** Felder; neue wurden ausdrücklich keine
+angelegt:
+
+| perinco `crm_leads` | ICON |
+| --- | --- |
+| `first_name`, `last_name`, `position` | gleichnamig |
+| `company` | wird als Unternehmensname angezeigt |
+| `email`, `phone` bzw. `mobile` | `email`, `phone` |
+| `linkedin`, `avatar`, `tags` | gleichnamig |
+| `kind` (Recruiter/Partner/Endkunde) | `kind` – **unverändert**, nicht auf die ICON-Arten umgebogen |
+| `source` | `lead_source` |
+| `notes` | `bio` |
+
+Bleiben drüben, weil ICON bei Kontakten kein Gegenstück hat: Anrede, Website,
+Anschrift. Ebenso Zuständiger und Ersteller – das sind Benutzer *jenes* Systems,
+deren Kennungen hier nichts bedeuten.
+
+Die Kennung bekommt ein Präfix (`perinco-31`), damit sie nie mit einer
+ICON-Kennung verwechselt wird. Sie ist eine Zeichenkette und gehört nicht in die
+Verweise der erweiterten Suche.
+
+### Wo sie auftauchen
+
+* **Kontaktliste und Kacheln** – grün umrandet, mit Herkunftsmerkmal
+* **Netzwerkansicht** – grün umrandete Kreise, über eine fein gestrichelte grüne
+ Kante an ihr Unternehmen gehängt. Die Verbindung läuft über den
+ **Firmennamen**, denn drüben steht dort nur Text und keine Kennung; groß/klein
+ wird ignoriert, damit „KfW" und „KFW" zusammenfinden. Passt kein Name, steht
+ der Kontakt für sich – das ist ehrlicher als eine geratene Zuordnung.
+* **Erweiterte Suche** – sie zählen mit, werden aber klar als perinco-Kontakte
+ benannt.
+
+Verbinden lassen sie sich nicht, und ihre Seitenleiste bietet keine Karte an –
+gepflegt werden sie drüben.
+
+> **Erfahrung aus dem Betrieb:** Ein zusätzlicher Filter in der Kontaktliste
+> kann die Liste still leeren und sieht dann aus wie Datenverlust. Deshalb sind
+> aktive Filter sichtbar ausgezeichnet, der Leerzustand benennt sie, und daneben
+> steht *Filter zurücksetzen*.