# Signatur-API: Integration mit Base44

Oeffentliche Dokumentation: [api.malymi.de](https://api.malymi.de/).
Die Markdown-Version fuer die Uebergabe an Base44 steht unter
[api.malymi.de/download](https://api.malymi.de/download) zum Download bereit.
Diese Domain dient nur der Dokumentation; die API-Basis steht im folgenden Abschnitt.

## Zweck und Betriebsstand

Die API verwendet die bestehende einfache elektronische Signatur des Vertrags-Tools:
handschriftlich gezeichnete Unterschrift oder Namenskuerzel mit ausdruecklicher Zustimmung.
Es handelt sich **nicht** um eine qualifizierte elektronische Signatur und **nicht**
um eine kryptografische PAdES-Signatur. Das Protokoll dokumentiert den Vorgang und die
SHA-256-Pruefsummen, bietet aber keinen qualifizierten Zeitstempel oder WORM-Speicher.

API-Basis:

```text
https://contracts.novastack-solutions.com/api/v1
```

Alle Requests sind serverseitig aus Base44 auszufuehren. API-Key und Webhook-Secret
gehoeren in die Backend-Secrets, niemals in Browser-Code, oeffentliche Entities oder URLs.
Es gibt absichtlich keine CORS-Freigabe fuer Browser-Aufrufe.

Der Betreiber legt pro Integration einen API-Client an und uebergibt API-Key und
Webhook-Secret einmalig auf sicherem Weg. Es existiert kein Standard-API-Key.
Die Webhook-Ziel-URL wird serverseitig hinterlegt; sobald die oeffentliche HTTPS-URL
des Base44-Webhook-Endpunkts feststeht, muss sie dem Betreiber mitgeteilt werden.
Bis dahin werden Ereignisse nicht an Base44 zugestellt (siehe Wiederholungen).

## Ablauf

1. Base44 erzeugt das PDF und kennt die exakten Signaturpositionen.
2. Base44 laedt PDF und JSON-Metadaten mit einem eindeutigen Idempotency-Key hoch.
3. Die API persistiert Original, Vorschau, Unterzeichner und Versandauftraege.
4. Der Worker verschickt jedem Unterzeichner einen persoenlichen Link. Die API-Antwort
   auf den Upload bedeutet noch nicht, dass eine E-Mail verschickt wurde.
5. Unterzeichner sehen das Dokument, ihren von Base44 gelieferten Namen und eine Liste
   ihrer Felder. Eine ausdruecklich bestaetigte Signatur wird in **alle** diesem
   Unterzeichner zugeordneten Felder eingesetzt.
6. Nach allen Unterschriften erzeugt der Worker das finale PDF und das PDF-Protokoll.
7. Erst nach erfolgreicher Speicherung wird `request.completed` mit Downloadlinks
   verschickt. Base44 laedt das finale PDF herunter und ordnet es ueber `external_id` zu.

Unterstuetzt werden 1 bis 10 Unterzeichner, parallel und ohne festgelegte Reihenfolge.
Alle Felder sind Pflichtfelder vom Typ `signature`. Optionale Felder, separate
Unterschriften pro Feld, Ablehnungen, Erinnerungen, OTP und qualifizierte Signaturen
sind nicht Teil dieser Version. Die bestehenden manuell erstellten Vertraege bleiben
unveraendert und sind nicht automatisch ueber die neue API abrufbar.

## Authentifizierung

```http
Authorization: Bearer CLIENT_ID.SECRET
```

Jeder Client sieht nur seine eigenen Vorgaenge. Ein deaktivierter Client kann weder
API, Signaturlinks noch Ergebnis-Downloadlinks verwenden.

## Vorgang anlegen

```http
POST /signature-requests
Authorization: Bearer CLIENT_ID.SECRET
Idempotency-Key: base44-vertrag-4711-version-1
Content-Type: multipart/form-data; boundary=...
```

Multipart-Teile:

| Name | Inhalt |
|---|---|
| `pdf` | Binaere PDF-Datei, maximal 20 MiB |
| `metadata` | JSON-Objekt als Text, maximal 128 KiB |

Beispiel fuer `metadata`:

```json
{
  "external_id": "base44-vertrag-4711",
  "title": "Dienstleistungsvertrag",
  "sender_name": "Musterfirma GmbH",
  "expires_in_days": 7,
  "signers": [
    {
      "id": "kunde",
      "name": "Max Mustermann",
      "email": "max@example.com"
    }
  ],
  "fields": [
    {
      "id": "unterschrift_kunde",
      "signer_id": "kunde",
      "type": "signature",
      "page": 3,
      "x": 70,
      "y": 620,
      "width": 180,
      "height": 60,
      "required": true
    }
  ]
}
```

### Pflichtangaben und Feldformat

- `external_id`: Pflicht, Base44-interne Zuordnung, maximal 180 Zeichen. Nicht
  automatisch eindeutig; unterschiedliche Dokumentversionen koennen dieselbe Referenz haben.
- `title`: Pflicht, maximal 180 Zeichen, wird in Einladung und Signierseite angezeigt.
- `sender_name`: Pflicht, maximal 180 Zeichen, sichtbarer Absendername und Grussformel.
  Die Absender-E-Mail-Adresse stammt ausschliesslich aus der bestehenden serverseitigen
  Mailkonfiguration. Base44 kann keine beliebige From-Adresse oder HTML-Mail vorgeben.
- `expires_in_days`: ganze Zahl von 1 bis 30, Standard 7, ab Erstellung des Vorgangs.
- `signers[].id`: eindeutige ID innerhalb des Vorgangs, 1 bis 64 Zeichen aus
  Buchstaben, Ziffern, `_` oder `-`.
- `signers[].name`: Pflicht, maximal 180 Zeichen; persoenliche Anrede und verbindlicher
  Name im Protokoll. Im Signierformular nicht editierbar.
- `signers[].email`: gueltige E-Mail-Adresse, maximal 254 Zeichen.
- `fields`: 1 bis 100 strukturierte Felddefinitionen; keine optische Erkennung,
  keine Textanker und kein Auslesen benannter AcroForm-Felder.
- `fields[].id`: eindeutige Feldbezeichnung innerhalb des Vorgangs, maximal 64 Zeichen.
- `fields[].signer_id`: muss auf einen gelieferten Unterzeichner verweisen.
- Jeder Unterzeichner braucht mindestens ein Feld.
- `page`: ganze Zahl, **erste Seite ist 1**.
- `x`, `y`, `width`, `height`: Zahlen in **PDF-Punkten**, 72 Punkte = 1 Zoll.
  Ein Millimeter entspricht `72 / 25.4` Punkten.
- Ursprung ist die **linke obere Ecke der sichtbaren, gedrehten Seite (CropBox)**.
  `x` waechst nach rechts, `y` nach unten. Bei einem PDF mit Seitenrotation gelten
  die Abmessungen so, wie die Seite im PDF-Viewer erscheint.
- `x` und `y` sind mindestens 0, Breite und Hoehe mindestens 10 Punkte.
  Das gesamte Rechteck muss innerhalb der angegebenen Seite liegen.
- Die Unterschrift wird proportional skaliert und mittig im Rechteck platziert.
  Es werden keine zusaetzlichen Namen oder Datumszeilen ausserhalb des Felds eingefuegt.
- `required` darf entfallen oder muss `true` sein; `false` wird abgewiesen.
- Nicht dokumentierte Metadaten-, Unterzeichner- und Feldattribute werden abgewiesen.

PDF: 1 bis 100 Seiten, unverschluesselt und ohne Passwort. Der freie FPDI-Parser
unterstuetzt keine komprimierten Cross-Reference-/Objektstreams aller PDF-Versionen;
bei `unsupported_pdf` bitte als unverschluesseltes PDF 1.4 ohne Objektstreams exportieren.
Formularinhalte muessen vor dem Upload in sichtbaren Seiteninhalt umgewandelt
(flattened) sein. Interaktive Widgets, Links, Anhaenge und vorhandene kryptografische
Signaturen werden beim Import nicht uebernommen. Keine bereits kryptografisch
unterschriebenen PDFs fuer diesen Ablauf verwenden. Das Original bleibt unveraendert
gespeichert; Vorschau und finale Datei sind neu aufgebaute PDFs mit importiertem Seiteninhalt.

### Idempotenz

`Idempotency-Key` ist Pflicht, 8 bis 128 Zeichen aus `A-Z a-z 0-9 _ . : -`.
Gleicher Client + gleicher Key + gleiche validierte Metadaten und PDF-Bytes liefert
denselben Vorgang zurueck, ohne erneut Einladungen anzulegen. Bei veraenderten Daten
folgt HTTP 409. Der Key gilt fuer die Lebensdauer des gespeicherten Vorgangs.
Nach einem Netzwerkfehler muss Base44 mit **demselben** Key und Inhalt wiederholen.
Die Reihenfolge der JSON-Eigenschaften ist unerheblich; die Reihenfolge der
Unterzeichner und Felder in den Arrays bleibt Teil des Inhaltsvergleichs.

### Beispiel mit curl

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $SIGNATURE_API_KEY" \
  -H "Idempotency-Key: base44-vertrag-4711-version-1" \
  -F "pdf=@vertrag.pdf;type=application/pdf" \
  -F "metadata=<metadata.json" \
  https://contracts.novastack-solutions.com/api/v1/signature-requests
```

HTTP 201 bei Erstellung (mit `Location`-Header auf den neuen Vorgang),
HTTP 200 bei idempotenter Wiederholung. Antwort:

```json
{
  "id": "sig_0123456789abcdef0123456789abcdef",
  "external_id": "base44-vertrag-4711",
  "title": "Dienstleistungsvertrag",
  "sender_name": "Musterfirma GmbH",
  "status": "pending",
  "created_at": "2026-10-20T10:00:00Z",
  "expires_at": "2026-10-27T10:00:00Z",
  "original_sha256": "...",
  "pages": [
    { "page": 1, "width": 595.28, "height": 841.89 }
  ],
  "fields": [],
  "signers": [
    {
      "id": "kunde",
      "name": "Max Mustermann",
      "email": "max@example.com",
      "status": "pending",
      "invitation_status": "queued"
    }
  ]
}
```

`pages` und `fields` enthalten in der echten Antwort alle Seiten und gelieferten Felder.
Persoenliche Signaturlinks und private Protokolldaten sind nicht in Statusantworten enthalten.

## Status und Downloads

```http
GET /signature-requests/{id}
GET /signature-requests/{id}/document
GET /signature-requests/{id}/audit
```

Alle drei Endpunkte verlangen den Bearer-Key. Die letzten beiden liefern PDF-Dateien,
aber nur bei `completed`; vorher HTTP 409.

| Status | Bedeutung |
|---|---|
| `pending` | Angelegt, noch keine Unterschrift |
| `partially_signed` | Mindestens eine, aber noch nicht alle Unterschriften gespeichert |
| `finalizing` | Alle Unterschriften gespeichert, PDF-Erstellung ausstehend |
| `completed` | Finales PDF und Protokoll erfolgreich gespeichert |
| `document_failed` | PDF-Erstellung fehlgeschlagen, Worker versucht es erneut |
| `expired` | Frist ohne vollstaendige Unterzeichnung abgelaufen |
| `cancelled` | Durch API abgebrochen |

Felder der Statusantwort (identisch fuer Upload, Status-GET, Abbruch und Webhook-`data`):

| Feld | Inhalt |
|---|---|
| `id`, `external_id`, `title`, `sender_name` | Vorgangsdaten |
| `status` | Vorgangsstatus, siehe Tabelle oben |
| `created_at`, `expires_at` | Zeitpunkte, ISO 8601 in UTC |
| `completed_at` | nur bei `completed` |
| `cancelled_at` | nur bei `cancelled` |
| `original_sha256` | SHA-256 des hochgeladenen Original-PDFs |
| `pages[]` | `page`, `width`, `height` in PDF-Punkten (sichtbare Seite) |
| `fields[]` | normalisierte Felddefinitionen wie geliefert |
| `signers[]` | `id`, `name`, `email`, `status`, `invitation_status`, optional `viewed_at`, `signed_at` |
| `document`, `audit` | nur bei `completed`: `download_url`, `expires_at`, `sha256` |

Optionale Felder fehlen, solange sie keinen Wert haben.

`signers[].status`: `pending` oder `signed`.
`invitation_status`: `queued`, `sent`, `retrying` oder `failed`.
`sent` bedeutet Annahme durch den Mailanbieter, **nicht** Postfachzustellung oder Lesen.

Bei `completed` enthaelt die Statusantwort zusaetzlich:

```json
{
  "completed_at": "2026-10-20T11:00:00Z",
  "document": {
    "download_url": "https://contracts.novastack-solutions.com/api/v1/downloads/sig_.../document?expires=...&signature=...",
    "expires_at": "2026-10-20T12:00:00Z",
    "sha256": "..."
  },
  "audit": {
    "download_url": "https://contracts.novastack-solutions.com/api/v1/downloads/sig_.../audit?expires=...&signature=...",
    "expires_at": "2026-10-20T12:00:00Z",
    "sha256": "..."
  }
}
```

Diese signierten Links benoetigen keinen Bearer-Key und sind daher wie Zugangsdaten
zu behandeln. Links aus Statusantworten gelten 1 Stunde, Links aus Webhooks 7 Tage.
Neue Links liefert jederzeit `GET /signature-requests/{id}`. Downloadlinks nicht als
dauerhafte Dokumentadresse speichern. Base44 sollte die Datei serverseitig herunterladen,
den SHA-256-Hash pruefen und in seinem geschuetzten Dateispeicher ablegen.
Der Bearer-Download ist alternativ ohne kurzlebigen Link moeglich.

## Abbrechen

```http
POST /signature-requests/{id}/cancel
```

Ohne Request-Body. Antwort ist die Statusantwort mit `status: "cancelled"`.
Nur fuer `pending` und `partially_signed`; wiederholter Abbruch liefert denselben
Status. Sonst HTTP 409 `invalid_state`. Bereits gespeicherte Teilunterschriften bleiben als Nachweis erhalten,
aber weitere Unterzeichnungen und finale Downloads werden verhindert.
`finalizing`, `completed`, `expired` und `document_failed` koennen nicht abgebrochen werden.

## Webhooks

Eine feste Ziel-URL und ein eigenes Webhook-Secret werden **pro API-Client**
serverseitig konfiguriert. Eine beliebige URL im Upload wird nicht akzeptiert.
Erlaubt ist ausschliesslich oeffentliches HTTPS auf Port 443; keine Weiterleitungen,
internen IPs oder HTTP-Basic-Zugangsdaten in der URL.

Ereignisse:

| Typ | Bedeutung |
|---|---|
| `request.created` | Vorgang gespeichert |
| `invitation.sent` | Mailanbieter hat Einladung angenommen |
| `invitation.failed` | Versandversuch fehlgeschlagen |
| `request.viewed` | Signierseite erstmals je Unterzeichner aufgerufen |
| `signer.signed` | Unterschrift eines Unterzeichners gespeichert |
| `request.completed` | Finale Dateien verfuegbar |
| `document.failed` | Finalisierung fehlgeschlagen |
| `request.expired` | Ablauf gespeichert (der Worker erkennt abgelaufene Vorgaenge automatisch, etwa minuetlich) |
| `request.cancelled` | Abbruch gespeichert |

Ein Seitenaufruf ist kein Nachweis persoenlicher Kenntnisnahme; auch Linkscanner
koennen `request.viewed` ausloesen.

```http
Content-Type: application/json
X-Signature-Timestamp: 1792494000
X-Signature-Signature: v1=HEX_HMAC_SHA256
X-Signature-Event-Id: evt_0123456789abcdef0123456789abcdef
```

```json
{
  "event_id": "evt_0123456789abcdef0123456789abcdef",
  "type": "request.completed",
  "occurred_at": "2026-10-20T11:00:00Z",
  "request_id": "sig_0123456789abcdef0123456789abcdef",
  "external_id": "base44-vertrag-4711",
  "signer_id": null,
  "data": {
    "id": "sig_0123456789abcdef0123456789abcdef",
    "status": "completed",
    "document": {
      "download_url": "https://contracts.novastack-solutions.com/api/v1/downloads/...",
      "expires_at": "2026-10-27T11:00:00Z",
      "sha256": "..."
    },
    "audit": {
      "download_url": "https://contracts.novastack-solutions.com/api/v1/downloads/...",
      "expires_at": "2026-10-27T11:00:00Z",
      "sha256": "..."
    }
  }
}
```

`data` ist die Statusantwort zum Ereigniszeitpunkt, einschliesslich weiterer oben
beschriebener Felder. Bei empfaengerbezogenen Ereignissen ist `signer_id` gesetzt.

### Verifikation und Zustellung

Signierter Inhalt ist:

```text
TIMESTAMP + "." + UNVERAENDERTER_HTTP_BODY
```

HMAC-SHA256 mit dem Webhook-Secret, hexadezimal und mit Praefix `v1=`.
Den Body **vor** JSON-Parsing als Text/Bytes lesen. Zeitstempel mit maximal
5 Minuten Abweichung pruefen und die Signatur in konstanter Zeit vergleichen.
Den Header `X-Signature-Event-Id` mit `event_id` aus dem signierten Body abgleichen.

Ereignisse werden mindestens einmal zugestellt, nicht garantiert in zeitlicher
Reihenfolge. Base44 muss `event_id` dauerhaft und eindeutig speichern und bereits
verarbeitete Ereignisse mit 2xx bestaetigen. Ereignisse schnell und dauerhaft
annehmen, dann die eigentliche Verarbeitung im Backend ausfuehren. Ein blosses
2xx vor dauerhafter Speicherung kann Ereignisse verlieren.

Erfolg ist jeder HTTP-Status 200 bis 299. Timeout nach 15 Sekunden. Nach Fehlern
erfolgen bis zu 7 Wiederholungen nach etwa 1, 5, 15, 60, 180, 360 und 720 Minuten
(jeweils Abstand zum vorherigen Versuch, plus Worker-Takt). Jede Zustellung hat
einen neuen Signaturzeitstempel; `event_id` und Body bleiben gleich.
Nach 8 Fehlversuchen wird der Auftrag als `failed` gespeichert.
Mailversand und Finalisierung verwenden dieselben Wiederholungsabstaende.
Nach einem Prozessabbruch ist auch ein erneuter Mailversand moeglich.

Wegen moeglicher verzoegerter Ereignisse nicht blind alte Statuswerte ueber neue
schreiben: Bei Bedarf den aktuellen Status per GET abgleichen. Ein Webhook-Fehler
veraendert den Abschlussstatus eines Dokuments nicht.

### Zustellprobleme abrufen und erneut versuchen

```http
GET /signature-requests/{id}/deliveries
POST /signature-requests/{id}/retry-deliveries
```

Antwort: `{ "deliveries": [...] }`, mit `id`, `kind` (`email`, `webhook`, `finalize`),
`state` (`pending`, `running`, `delivered`, `failed`), `attempts`, `due_at`
(Unix-Sekunden), `last_error` und `created_at`. Maximal 500 Eintraege pro Vorgang.
`delivered` kann bei nicht mehr erforderlichen Mailauftraegen auch bedeuten, dass
sie ohne Versand abgeschlossen wurden, z.B. nach Abbruch oder bereits erfolgter Signatur.
POST (ohne Body) setzt alle ausgeschoepften `failed`-Auftraege dieses Vorgangs zurueck
und liefert danach dieselbe `deliveries`-Liste.
Bereits wartende Auftraege behalten ihre geplante Wiederholung.
Sehr alte Webhook-Downloadlinks koennen beim manuellen Replay abgelaufen sein;
in diesem Fall neue Links per Status-GET abrufen.

## Base44-Backend: Upload-Beispiel

### Eigenen Webhook in Base44 anlegen

Ein Base44-Projekt ist nicht auf einen einzigen Endpunkt begrenzt. Jede
veroeffentlichte Backend-Funktion erhaelt einen eigenen HTTP-Endpunkt.
Fuer diese Integration eine eigene Funktion `signatureWebhook` anlegen.
Laut aktueller Base44-Entwicklerdokumentation lautet das URL-Schema:

```text
https://DEINE-APP-DOMAIN/functions/signatureWebhook
```

Die tatsaechlich fuer euer Projekt bereitgestellte URL aus der Base44-Oberflaeche
uebernehmen; nicht allein aus der Frontend-URL ableiten. Unter Dashboard > API
zeigt der Bereich Backend Functions die aufrufbaren Funktionen.
Die normale App-Startseite bleibt bestehen; andere Integrationen erhalten
weitere eigene Funktionen und Endpunkte.

Die Funktion muss unseren POST-Request ohne interaktiven Benutzer-Login annehmen,
zuerst die HMAC-Signatur pruefen und erst danach Daten lesen/schreiben.
Base44-Aufrufe ueber direkte HTTP-Webhooks haben keinen angemeldeten Benutzer;
falls `asServiceRole` benoetigt wird, ausschliesslich innerhalb dieser
serverseitigen Funktion und nach erfolgreicher Signaturpruefung verwenden.
Geheime Werte nur serverseitig speichern.

Moeglicher Auftrag an den Base44-Editor:

```text
Erstelle eine Backend-Funktion signatureWebhook fuer externe POST-Webhooks
unserer Signatur-API. Verwende den unveraenderten Request-Body und pruefe
X-Signature-Timestamp (maximal 5 Minuten Abweichung) und
X-Signature-Signature (v1=HMAC-SHA256 ueber timestamp + "." + body)
gegen das Backend-Secret SIGNATURE_WEBHOOK_SECRET.
Gleiche X-Signature-Event-Id mit event_id im signierten Body ab.
Akzeptiere nur korrekt signierte Requests, ohne Benutzer-Login zu verlangen.
Speichere verifizierte Ereignisse dauerhaft und dedupliziere nach event_id;
bestaetige erst danach mit 2xx. Ordne sie ueber external_id und request_id
dem passenden Vorgang zu. Bei request.completed liegen die Downloadlinks
unter data.document.download_url und data.audit.download_url.
Lade Dateien serverseitig herunter und pruefe die gelieferten SHA-256-Hashes.
Gib mir nach dem Deployment die tatsaechliche oeffentliche HTTPS-URL der Funktion.
Nutze fuer den vollstaendigen Vertrag die beigefuegte API.md.
```

Offizielle Referenzen:
[Backend Functions, HTTP-Endpunkte](https://docs.base44.com/developers/backend/resources/backend-functions/overview#via-http)
und [API-Seite im App-Dashboard](https://docs.base44.com/Building-your-app/your-apps-api-page).

### PDF aus einer Backend-Funktion hochladen

Dies ist plattformneutraler Fetch-Code fuer eine serverseitige Funktion. Wie Secrets,
PDF-Dateizugriff, Aufruferberechtigungen und Webhook-Routen in eurem konkreten
Base44-Projekt eingebunden werden, muss dort passend verdrahtet werden.

```javascript
async function submitSignatureRequest({ pdfBytes, metadata, apiKey, versionKey }) {
  const form = new FormData();
  form.append("pdf", new Blob([pdfBytes], { type: "application/pdf" }), "vertrag.pdf");
  form.append("metadata", JSON.stringify(metadata));

  const response = await fetch(
    "https://contracts.novastack-solutions.com/api/v1/signature-requests",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Idempotency-Key": versionKey
      },
      body: form
    }
  );
  const result = await response.json();
  if (!response.ok) {
    throw new Error(`${response.status}: ${JSON.stringify(result.error)}`);
  }
  return result; // id und external_id im Base44-Vorgang speichern
}
```

`Content-Type` fuer FormData nicht manuell setzen; Fetch ergaenzt die Boundary.
Vor dem Aufruf die Base44-Berechtigung fuer den betreffenden Vorgang pruefen.

### Webhook-HMAC mit Web Crypto pruefen

```javascript
async function verifySignatureWebhook(request, webhookSecret) {
  const rawBody = await request.text();
  const timestamp = request.headers.get("X-Signature-Timestamp") ?? "";
  const signature = request.headers.get("X-Signature-Signature") ?? "";
  if (!/^\d+$/.test(timestamp)
      || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300
      || !/^v1=[a-f0-9]{64}$/.test(signature)) {
    throw new Error("Invalid webhook timestamp or signature format");
  }

  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw", encoder.encode(webhookSecret),
    { name: "HMAC", hash: "SHA-256" }, false, ["verify"]
  );
  const bytes = Uint8Array.from(
    signature.slice(3).match(/../g), pair => parseInt(pair, 16)
  );
  const valid = await crypto.subtle.verify(
    "HMAC", key, bytes, encoder.encode(`${timestamp}.${rawBody}`)
  );
  if (!valid) throw new Error("Invalid webhook signature");
  const event = JSON.parse(rawBody);
  if (event.event_id !== request.headers.get("X-Signature-Event-Id")) {
    throw new Error("Invalid event ID");
  }
  return event;
}
```

Der Webhook muss von unserem Server ohne Base44-Benutzersitzung erreichbar sein.
Seine Authentifizierung erfolgt ueber die obige HMAC-Pruefung, nicht durch eine
oeffentlich freigegebene Service-Role oder einen API-Key im Querystring.

## Fehlerformat

```json
{
  "error": {
    "code": "invalid_fields",
    "message": "Signaturfeld liegt ausserhalb der Seite: unterschrift_kunde"
  }
}
```

| HTTP | Beispiele |
|---|---|
| 401 | Fehlender, falscher oder deaktivierter API-Key |
| 403 | Falscher/abgelaufener Downloadlink |
| 404 | Vorgang fehlt oder gehoert zu anderem Client |
| 405 | Falsche HTTP-Methode |
| 409 | Idempotenzkonflikt oder unpassender Vorgangsstatus |
| 415 | Kein Multipart-Upload |
| 422 | Ungueltige Metadaten, PDF, Felder oder Uploadfehler |
| 500 | Interner Fehler; Details nur im Serverlog |
| 503 | API noch nicht konfiguriert |

Maschinenlesbare `error.code`-Werte (fuer Programmlogik verwenden, `message` nur anzeigen):

| HTTP | `code` | Ursache |
|---|---|---|
| 401 | `unauthorized` | Bearer-Key fehlt, falsch oder Client deaktiviert |
| 403 | `invalid_download` | Signierter Downloadlink falsch oder abgelaufen |
| 404 | `not_found` | Endpunkt/Vorgang nicht vorhanden oder fremder Client |
| 405 | `method_not_allowed` | Falsche HTTP-Methode |
| 409 | `idempotency_conflict` | Idempotency-Key bereits mit anderen Daten verwendet |
| 409 | `not_completed` | Dokument/Protokoll vor `completed` angefordert |
| 409 | `invalid_state` | Abbruch in diesem Status nicht moeglich |
| 415 | `unsupported_media_type` | Kein `multipart/form-data` |
| 422 | `invalid_idempotency_key` | Header fehlt oder Format ungueltig |
| 422 | `upload_failed` | PDF-Teil fehlt oder Upload abgebrochen/zu gross |
| 422 | `invalid_metadata` | Metadaten-JSON, Unterzeichner oder Feldangaben ungueltig |
| 422 | `invalid_pdf` | Keine PDF-Datei, ueber 20 MiB oder nicht 1 bis 100 Seiten |
| 422 | `unsupported_pdf` | PDF verschluesselt oder vom Parser nicht lesbar |
| 422 | `invalid_fields` | Feld ausserhalb der Seite oder Seite nicht vorhanden |
| 500 | `internal_error` | Interner Fehler |
| 503 | `not_configured` | API noch nicht konfiguriert |

Webserver/Proxy koennen uebergrosse Requests bereits vor PHP mit 413 abweisen.
Netzwerkfehler und 5xx beim Anlegen mit gleichem Idempotency-Key wiederholen.
4xx anhand Fehlercode korrigieren, nicht unveraendert endlos wiederholen.

## Betrieb und Einrichtung

Voraussetzungen: PHP 8.2 mit curl, gd, mbstring, pdo_sqlite und den Composer-
Abhaengigkeiten aus `composer.lock`. Apache benoetigt mod_rewrite und AllowOverride All.
`.htaccess` schuetzt interne Dateien und routet `/api/v1/...`; `.user.ini` enthaelt
die Upload-Limits fuer FPM. Bei einem anderen Webserver muessen diese Regeln
explizit uebernommen werden. Standard-PHP-Limits von 2 MiB reichen nicht aus.

```bash
cd /var/www/vertrags-signatur-tool
composer install --no-dev --prefer-dist --no-interaction
sudo php manage_api.php init
sudo php manage_api.php create-client base44 \
  https://DEIN-BASE44-BACKEND/DEIN-WEBHOOK \
  /root/base44-signature-credentials.json
```

`init` nicht erneut ausfuehren, wenn bereits eingerichtet.
Das Credentials-File ist nur fuer root lesbar. Die beiden Secrets sicher in die
Base44-Backend-Secrets uebertragen und die Transferdatei danach sicher entfernen.
Es werden keine Secrets in der Konsole ausgegeben.
Konfiguration: `/etc/contracts-signature-api.json`, root:www-data, Modus 0640.
Private Daten: `/var/lib/contracts-signature-api`, www-data, Modus 0700.
Bei Konfigurationsaenderungen Datei atomar ersetzen, Berechtigungen beibehalten.
`SIGNATURE_API_CONFIG` kann fuer isolierte Tests auf eine andere Konfiguration zeigen.

```bash
sudo php manage_api.php set-webhook base44 https://NEUE-URL/WEBHOOK
sudo php manage_api.php disable-client base44
sudo -u www-data php worker.php
```

Fuer Key-Rotation neuen Client anlegen und den alten nach Umstellung deaktivieren.
Vorgaenge sind an ihre urspruengliche Client-ID gebunden; alte Vorgaenge benoetigen
bis zum Abschluss weiterhin den alten Zugang.

Cron: `/etc/cron.d/contracts-signature-api` startet den Worker jede Minute.
Worker sind durch einen Prozesslock gegen parallele Verarbeitung geschuetzt.
Pro Lauf werden maximal 50 Auftraege bearbeitet; bei hoeherem Volumen Takt/Batch
und einen dedizierten Dienst bewusst anpassen. PHP-/Cron-Fehler werden im
Serverlog bzw. Cron-Fehlerkanal protokolliert; fehlgeschlagene Zustellungen zusaetzlich
in der Datenbank. Zustellfehler und freie Speicherkapazitaet betrieblich ueberwachen.
Bestehende SendGrid-Konfiguration wird verwendet; keine echten E-Mails fuer Tests
an fremde Empfaenger schicken.

SQLite verwendet WAL und Transaktionen; Backups muessen DB, WAL und private
Dokumentdateien konsistent erfassen, z.B. waehrend einer geplanten Schreibpause.
Aufbewahrung und Loeschfristen, AV-Vertrag, Zugriffsberechtigungen und Backups
muessen fuer die konkreten Vertragsdaten betrieblich festgelegt werden. Es gibt
absichtlich keine automatische Loeschung ohne festgelegte Aufbewahrungsregel.
IP und User-Agent werden nur im geschuetzten Protokoll gespeichert; ohne
explizit konfigurierte Proxy-Vertrauenskette wird `REMOTE_ADDR` verwendet.

## Validierung

```bash
php tests/api_test.php
```

Der Test verwendet eine temporaere Konfiguration und private Testdatenbank,
einen kurzlebigen lokalen HTTP-Server, Mock-Transporte statt echter E-Mails/Webhooks
und raeumt seine Dateien und Prozesse auf.
Fuer unabhaengige visuelle Feldpruefung muss `pdftoppm` aus `poppler-utils`
installiert sein. Die Produktion wird durch diesen Test nicht beschrieben.

Vor Freigabe einer Base44-Anwendung zusaetzlich mit einem repraesentativen echten
PDF, einem kontrollierten Postfach und dem echten Webhook testen:
Anrede, Seiten/Feldpositionen, Mailannahme, Unterzeichnung, HMAC, Deduplizierung,
Download/Hash und Zuordnung zum Base44-Vorgang.
