# Eingebettetes Signieren

Ihre Kunden unterschreiben direkt in Ihrer Anwendung: Sie zeigen unsere Signierseite in einem `iframe` auf Ihrer eigenen Website. Das geht in allen Stufen, in denen externe Empfänger handeln (EES, FES), und für Freigebende und Betrachter. **Qualifizierte Unterschriften (QES) werden nie eingebettet** – Mitglieder mit Zertifikat unterschreiben im Dashboard oder über den Freigabe-Link.

Maßgeblich für Ihre Geschäftslogik bleibt der **Webhook** (bzw. `GET /v1/events`). Die Nachrichten der eingebetteten Seite sind Hinweise für Ihre Oberfläche, keine Bestätigung.

## 1. Erlaubte Websites festlegen

Nur Websites auf der Liste Ihrer Organisation dürfen die Signierseite einbetten. Die Liste ist leer, bis Sie sie füllen – leer heißt: Einbettung aus.

```http
PUT /v1/embed-settings
Authorization: Bearer qgw_live_…
Idempotency-Key: 5d1f…
Content-Type: application/json

{ "allowedOrigins": ["https://app.example.com", "https://*.beispiel.de"] }
```

Antwort:

```json
{ "object": "embed_settings", "allowedOrigins": ["https://app.example.com", "https://*.beispiel.de"] }
```

Regeln für jeden Eintrag:

- `https://host[:port]` ohne Pfad, Query oder Zugangsdaten, oder `https://*.host` für alle Unterdomains einer **eigenen** Domain (nicht `*.vercel.app`, nicht `*.co.uk` und ähnliche öffentliche Endungen).
- Kein `*` allein, keine IP-Adressen, kein `http://` – außer `http://localhost:<port>` mit einem **Test-Schlüssel**; solche Einträge gelten nur im Testmodus.
- Höchstens 20 Einträge, jeder nur einmal. Ein abgelehnter Eintrag ergibt 422 / 9012 mit `details.reason` (`origin_invalid`, `origin_duplicate`) und `details.index`.
- Scope `org:manage` zum Ändern (Standard aus), `org:read` zum Lesen (`GET /v1/embed-settings`). Im Dashboard ändern Inhaber und Administratoren die Liste.
- Jede Änderung steht im Aktivitätsprotokoll (`EMBED_SETTINGS_UPDATED`).

Änderungen gelten sofort für unsere Signierseite und für neue Einbettungs-Sitzungen. Für den Unterschriftsschritt bei EES/FES erhält der Signaturdienst die Liste beim Start der Unterschriftsrunde; eine spätere Änderung wirkt erst für neue Runden. Dort zählen nur exakte Origins (Einträge mit `*` erlauben nur unsere Seite, nicht den Unterschriftsschritt), und je Runde höchstens neun Ihrer Origins.

Für Signierseite, Einbettungs-Sitzungen und Unterschriftsschritt gilt dieselbe Regel je Modus: Im Live-Modus zählen nur die `https`-Einträge, `http://localhost:<port>` und andere Nur-Test-Einträge ausschließlich im Testmodus. Ist die Einbettung für Ihre Organisation ausgeschaltet (Stufenfunktion) oder im Live-Modus noch gesperrt, erhält auch der Unterschriftsschritt keine Ihrer Origins. Qualifizierte Unterschriften (QES) werden nie eingebettet.

## 2. Einbettungs-Sitzung anfordern

Sobald ein Empfänger an der Reihe ist („dran“, Ereignis `recipient.ready`), fordert Ihr Server eine Sitzung für die Website an, auf der Sie die Seite zeigen:

```http
POST /v1/envelopes/{id}/recipients/{rid}/embedded-session
Authorization: Bearer qgw_test_…
Idempotency-Key: 8c2e…
Content-Type: application/json

{ "origin": "https://app.example.com", "locale": "de" }
```

```json
{
  "object": "embedded_session",
  "url": "https://sign.<domain>/s/<token>?embed=1",
  "expiresAt": "2026-09-30T12:05:00Z",
  "origin": "https://app.example.com"
}
```

- Scope `envelopes:signing_urls` (Standard aus), Rate-Limit `write`.
- Die URL gilt **5 Minuten und genau einmal** und nur eingebettet auf **dieser** Origin. Beim ersten Laden tauscht die Seite sie gegen eine Sitzung, die sie nur im Speicher hält; ein zweiter Aufruf zeigt „Dieser Link ist nicht gültig“, ohne zu verraten, ob es den Vorgang gibt. Fordern Sie für jede Anzeige eine neue Sitzung an.
- Der Link der Einladungsmail bleibt gültig.
- Die Antwort wird nie gespeichert (sie enthält einen Link): Ein wiederholter Aufruf mit demselben `Idempotency-Key` legt eine neue Sitzung an; der Schlüssel schützt nur vor gleichzeitigen Doppelaufrufen (409 / 9008).
- Behandeln Sie die URL wie ein Passwort: nicht loggen, nicht weitergeben, nicht in Ihrer Browser-Anwendung erzeugen (nur auf Ihrem Server).
- Jede Sitzung steht im Nachweisprotokoll des Vorgangs (`LINK_ISSUED`, ohne die URL).

Mögliche Fehler, in dieser Reihenfolge geprüft:

| Status / Code | Bedeutung |
|---|---|
| 404 | Vorgang oder Empfänger gehören nicht zu Ihrer Organisation oder nicht zum Modus des Schlüssels |
| 403 / 9039 | Einbettung ist für Ihre Organisation abgeschaltet |
| 403 / 9038 | `origin` steht nicht auf der Liste (auch: Platzhalter statt exakter Origin, `http://localhost` im Live-Modus) |
| 422 / 9012 `qes_not_embeddable` | Der Empfänger unterschreibt qualifiziert (QES) |
| 409 / 9033 | Der Empfänger ist nicht an der Reihe (`details.reason`) |
| 409 / 9030 `signing_page_disabled` | Die Signierseite ist abgeschaltet |
| 403 / 9030 `embedding_live_disabled` | Live-Schlüssel: Einbettung ist für den Live-Betrieb noch nicht freigegeben |
| 403 | Dem Schlüssel fehlt `envelopes:signing_urls` |

Ein Signierlink (`POST …/signing-url`) oder der Link der Einladungsmail lässt sich ebenfalls einbetten: Hängen Sie `?embed=1` an. Dann darf jede Website Ihrer Liste die Seite zeigen, und der Link gilt wie gewohnt (15 Minuten bis zum ersten Öffnen, danach 60 Minuten Sitzung). Die Einbettungs-Sitzung ist vorzuziehen, weil sie an genau eine Website gebunden ist.

## 3. Seite anzeigen mit dem Widget

```html
<div id="unterschrift"></div>
<script src="https://sign.<domain>/embed/v1/onesign.js"></script>
<script>
  const widget = OneSign.mountSigning(document.getElementById('unterschrift'), {
    url: sessionUrl, // die URL aus Schritt 2, von Ihrem Server
    minHeight: 600,
    title: 'Mietvertrag unterschreiben',
    onEvent(event) {
      if (event.type === 'completed') zeigeAbschluss();
      if (event.type === 'declined') zeigeAblehnung();
      if (event.type === 'error') zeigeFehler(event.code);
    },
  });
  // widget.destroy() entfernt das iframe wieder.
</script>
```

Das Widget hat keine Abhängigkeiten und setzt keine Cookies. Es erzeugt das `iframe` mit `allow="camera; microphone; fullscreen"` (die Identitätsprüfung der FES braucht die Kamera auf jeder Rahmen-Ebene) und `referrerpolicy="no-referrer"`, passt die Höhe an (`resize`) und gibt nur Nachrichten weiter, die von der Origin der Signierseite stammen und zum Protokoll passen.

Ohne Widget: Ein eigenes `iframe` mit denselben Attributen funktioniert ebenso; hören Sie dann selbst auf `message` und prüfen Sie `event.origin` gegen die Origin der URL sowie `source` und `v` der Nachricht.

## 4. Nachrichten der Seite

Jede Nachricht hat die Form `{ source: "onesign", v: 1, type, …, at }` und enthält **nie** Namen, E-Mail-Adressen, Tokens oder Links. Die Seite schickt sie nur an die gebundene Origin (bzw. die geprüfte Website Ihrer Liste), nie an `*`.

| `type` | Zusatzfelder | Bedeutung |
|---|---|---|
| `ready` | `envelopeId`, `recipientId` | Seite geladen |
| `resize` | `height` | Höhe der Seite in Pixeln |
| `signed` | `envelopeId`, `recipientId` | Der Empfänger hat unterschrieben |
| `approved` | `envelopeId`, `recipientId` | Der Empfänger hat freigegeben |
| `viewed` | `envelopeId`, `recipientId` | Der Empfänger hat das Ansehen bestätigt |
| `declined` | `envelopeId`, `recipientId` | Der Vorgang wurde abgelehnt (auch im Unterschriftsschritt) |
| `completed` | `envelopeId` | Der Vorgang ist abgeschlossen |
| `expired` | `envelopeId`, `recipientId` | Der Vorgang ist abgelaufen |
| `error` | `code` | `link_invalid`, `rate_limited`, `unavailable`, `action_failed`, `envelope_cancelled`, `envelope_failed`, `qes_new_window` |

`envelopeId` und `recipientId` stehen nur in Nachrichten einer Einbettungs-Sitzung; bei einem Signierlink mit `?embed=1` fehlen sie. Die Seite leitet im eingebetteten Modus nie selbst weiter – reagieren Sie auf `completed` (oder den Webhook). Datenschutzhinweis und Fußzeile mit Impressum bleiben im Rahmen sichtbar.

## 5. Sicherheit und Browser

- **Clickjacking-Schutz:** Die Signierseite erlaubt das Einbetten (`Content-Security-Policy: frame-ancestors`) nur der gebundenen Origin bzw. den Websites Ihrer Liste; ohne `?embed=1` ist sie nie einbettbar.
- **Keine Cookies:** Browser blockieren Cookies in fremden Rahmen (z. B. Safari). Die Seite braucht keine – die Sitzung lebt im Speicher der Seite.
- **QES:** Öffnet ein eingebetteter Link den Schritt eines Mitglieds mit qualifizierter Unterschrift, zeigt die Seite „In neuem Fenster anmelden“ (öffnet ein neues Fenster) und meldet `error` mit `qes_new_window`.
- **Live-Betrieb:** Im Live-Modus ist die Einbettung noch gesperrt (403 / 9030 `embedding_live_disabled`), bis sie auf echten Geräten geprüft ist. Im Testmodus steht alles zur Verfügung.
