# Beispielcode

> Vollständige Node.js- und Python-Programme für die Fileverity-API: ein Bild prüfen, ein großes PDF prüfen, von KI verfassten Text erkennen und Webhook-Signaturen prüfen.

Quelle: https://fileverity.com/de/docs/sample-code

Diese Seite enthält vier vollständige Programme für die Fileverity-API, jeweils in Node.js ab Version 18 und in Python ab Version 3.9: ein Bild prüfen und auf das Ergebnis warten, ein großes PDF prüfen, von KI verfassten Text erkennen und Webhooks mit Signaturprüfung empfangen. Kopieren Sie eines, setzen Sie `FILEVERITY_API_KEY` und führen Sie es aus.

Die Programme rufen die HTTPS-API direkt auf, mit dem in Node.js integrierten `fetch` und dem Paket `requests` in Python, sodass Sie nur einen HTTP-Client brauchen. Die Aufrufe, Felder und Statuswerte entsprechen denen, die in [Dateien](https://fileverity.com/de/docs/files.md), [Analysen](https://fileverity.com/de/docs/analyses.md), [Textprüfungen](https://fileverity.com/de/docs/text-detections.md) und [Webhooks](https://fileverity.com/de/docs/webhooks.md) beschrieben sind.

| Programm | Was es tut | Aufrufe |
| --- | --- | --- |
| [Ein Bild prüfen](https://fileverity.com/de/docs/sample-code.md#image) | Lädt ein Bild hoch, wartet auf das Ergebnis und gibt die Zusammenfassung und die Berichts-URL aus. | `POST /files`, `POST /analyses`, `GET /analyses/{id}` |
| [Ein großes PDF prüfen](https://fileverity.com/de/docs/sample-code.md#pdf) | Lädt ein Dokument über eine vorsignierte URL hoch, fragt per Polling ab, bis die Prüfung abgeschlossen ist, und speichert den PDF-Bericht. | `POST /files`, `GET /files/{id}`, `POST /analyses`, `GET /analyses/{id}`, `GET /analyses/{id}/report.pdf` |
| [Von KI verfassten Text erkennen](https://fileverity.com/de/docs/sample-code.md#text) | Sendet eine Textdatei und gibt die Einstufung, den Score und jede von KI verfasste Passage aus. | `POST /text/detections` |
| [Webhooks empfangen](https://fileverity.com/de/docs/sample-code.md#webhooks) | Prüft die Signatur jeder Zustellung, ignoriert Duplikate und verarbeitet die Ereignisse. | Ihr Endpunkt, aufgerufen mit `analysis.completed`, `analysis.failed` und `balance.low` |

## Vor dem Ausführen

- Erstellen Sie in der Konsole unter [API-Schlüssel](https://fileverity.com/console/keys?lang=de) einen Schlüssel im Modus **Test**. Ein Testschlüssel liefert feste Beispielergebnisse und wird nie berechnet, sodass jedes Programm kostenlos läuft. Um echte Dateien zu prüfen, erstellen Sie einen Schlüssel im Modus **Live**. Die Programme bleiben unverändert.
- Node.js: Speichern Sie jedes Programm als `.mjs`-Datei, damit es `import` und `await` auf oberster Ebene nutzen kann, und führen Sie es mit `node` aus. Es muss nichts installiert werden.
- Python: Führen Sie `pip install requests` aus. Der Webhook-Empfänger benötigt zusätzlich `pip install flask`.

**Schlüssel festlegen: Shell**

```bash
export FILEVERITY_API_KEY=fv_test_...
```

**Schlüssel festlegen: PowerShell**

```powershell
$env:FILEVERITY_API_KEY = "fv_test_..."
```

## Wie die Programme mit Schlüsseln, Fehlern und Wiederholungen umgehen

Die drei Programme, die die API aufrufen, verwenden dieselben Funktionen `request` und `api`. Übernehmen Sie sie unverändert in Ihren eigenen Code.

| Aspekt | Was die Programme tun |
| --- | --- |
| Schlüssel | Wird aus `FILEVERITY_API_KEY` gelesen und nie in den Code geschrieben. Rufen Sie die API von Ihrem Server aus auf, nicht aus Browser-Code. |
| Idempotenz | Jede POST-Anfrage sendet einen `Idempotency-Key`, eine neue UUID pro Anfrage, und verwendet ihn bei jeder Wiederholung erneut, sodass eine Wiederholung eine Prüfung nie zweimal ausführt oder berechnet. Damit auch ein erneuter Lauf des gesamten Skripts sicher ist, leiten Sie den Schlüssel stattdessen aus Ihrer eigenen Datensatz-ID ab. Siehe [Idempotenz](https://fileverity.com/de/docs/idempotency.md). |
| Fehler | Eine Antwort ohne `2xx`-Status enthält `error.type`, `error.code`, `error.message`, `error.param` und `error.request_id`. Die Programme brechen mit `code: message (request request_id)` ab. Verzweigen Sie anhand von `code` und geben Sie dem Support die Anfrage-ID an. Siehe [Fehler](https://fileverity.com/de/docs/errors.md). |
| Wiederholungen | Ein Netzwerkfehler, ein `5xx`-Status (`internal_error`, `analysis_failed`, `service_unavailable`), `rate_limited` (429) und `idempotency_in_progress` (409) werden mit bis zu 5 Versuchen wiederholt. Gewartet wird die Zeit aus dem Header `retry-after`, wenn die Antwort einen enthält, sonst 2, 4, 8 und 16 Sekunden. Jeder andere `4xx`-Status beendet das Programm sofort. `pending_files_limit` ist ebenfalls ein 429, hat aber kein `retry-after` und wird deshalb nicht wiederholt. |
| Rate-Limit | Meldet eine Antwort `x-ratelimit-remaining-requests: 0`, wartet das Programm `x-ratelimit-reset-requests` Sekunden bis zum nächsten Aufruf, damit kein Fehler 429 auftritt. Siehe [Rate-Limits und Header](https://fileverity.com/de/docs/rate-limits.md#headers). |
| Timeouts | Die Python-Programme setzen pro Anfrage ein Timeout von 90 Sekunden, länger als die 60 Sekunden, die `Prefer: wait` eine Anfrage offenhalten kann. |

## Ein Bild prüfen und auf das Ergebnis warten

Das Programm lädt das Bild als Multipart hoch, erstellt mit `Prefer: wait=60` eine Analyse, sodass die Anfrage offen bleibt, bis das Ergebnis vorliegt, und gibt die Zusammenfassung und die Berichts-URL aus. Endet das Warten vorher, antwortet die API mit `202` und `status: processing`, und das Programm fragt `GET /analyses/{id}` alle 3 Sekunden ab. Siehe [Warten, Polling oder Webhooks](https://fileverity.com/de/docs/analyses.md#wait).

Speichern Sie es als `check-image.mjs` oder `check_image.py` und führen Sie `node check-image.mjs photo.jpg` oder `python check_image.py photo.jpg` aus. Multipart nimmt Bilder bis 4 MB an. Für ein größeres Bild bis 10 MB verwenden Sie den Upload über eine vorsignierte URL aus dem nächsten Programm und geben Sie den Content-Type des Bildes an, zum Beispiel `image/jpeg`.

**Ein Bild prüfen: JavaScript**

```javascript
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { basename } from "node:path";

const base = "https://api.fileverity.com/v1";
const key = process.env.FILEVERITY_API_KEY;
const path = process.argv[2];
if (!key || !path) throw new Error("Usage: FILEVERITY_API_KEY=... node check-image.mjs photo.jpg");

const retryable = ["rate_limited", "idempotency_in_progress"];
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function request(url, init) {
  for (let attempt = 1; ; attempt++) {
    let response = null;
    let problem;
    try {
      response = await fetch(url, init);
      if (response.ok) {
        const remaining = response.headers.get("x-ratelimit-remaining-requests");
        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));
        if (remaining === "0") await sleep(reset);
        return response;
      }
      problem = (await response.json().catch(() => ({}))).error ?? {};
    } catch (error) {
      problem = { code: "network_error", message: error.message };
    }
    const status = response?.status ?? 0;
    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);
    if (!retry || attempt === 5) {
      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;
      throw new Error(`${code}: ${message} (request ${id})`);
    }
    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);
  }
}

async function api(method, route, { headers, body } = {}) {
  const auth = { Authorization: `Bearer ${key}` };
  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });
  return response.json();
}

const form = new FormData();
form.append("file", new Blob([await readFile(path)]), basename(path));
const file = await api("POST", "/files", {
  headers: { "Idempotency-Key": randomUUID() },
  body: form,
});

let analysis = await api("POST", "/analyses", {
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({ model: "fvimg-2", file: file.id }),
});
while (analysis.status === "processing") {
  await sleep(3);
  analysis = await api("GET", `/analyses/${analysis.id}`);
}

if (analysis.status !== "succeeded") {
  throw new Error(`The check ${analysis.status}: ${analysis.error?.message}`);
}
const { level, action, assessment, ai, edit } = analysis.summary;
console.log(`${analysis.id} ${analysis.status}`);
console.log(`level ${level}, action ${action}, assessment ${assessment}`);
console.log(`AI generation: ${ai}, editing: ${edit}`);
console.log(`Report: ${analysis.report_url}`);
```

**Ein Bild prüfen: Python**

```python
import os
import sys
import time
import uuid

import requests

BASE = "https://api.fileverity.com/v1"
KEY = os.environ.get("FILEVERITY_API_KEY")
if not KEY or len(sys.argv) < 2:
    sys.exit("Usage: FILEVERITY_API_KEY=... python check_image.py photo.jpg")

RETRYABLE = ("rate_limited", "idempotency_in_progress")


def request(method, url, **kwargs):
    for attempt in range(1, 6):
        try:
            response = requests.request(method, url, timeout=90, **kwargs)
        except requests.RequestException as error:
            response, problem = None, {"code": "network_error", "message": str(error)}
        else:
            if response.ok:
                if response.headers.get("x-ratelimit-remaining-requests") == "0":
                    time.sleep(int(response.headers["x-ratelimit-reset-requests"]))
                return response
            try:
                problem = response.json()["error"]
            except (ValueError, KeyError, TypeError):
                problem = {}
        status = response.status_code if response is not None else 0
        retry = status == 0 or status >= 500 or problem.get("code") in RETRYABLE
        if not retry or attempt == 5:
            code = problem.get("code", status)
            message = problem.get("message", "Request failed")
            request_id = problem.get("request_id", "unknown")
            raise RuntimeError(f"{code}: {message} (request {request_id})")
        wait = response.headers.get("retry-after") if response is not None else None
        time.sleep(int(wait or 2**attempt))


def api(method, route, headers=None, **kwargs):
    headers = {"Authorization": f"Bearer {KEY}", **(headers or {})}
    return request(method, BASE + route, headers=headers, **kwargs).json()


with open(sys.argv[1], "rb") as source:
    data = source.read()
upload = {"file": (os.path.basename(sys.argv[1]), data)}
file = api("POST", "/files", headers={"Idempotency-Key": str(uuid.uuid4())}, files=upload)

analysis = api(
    "POST",
    "/analyses",
    headers={"Idempotency-Key": str(uuid.uuid4()), "Prefer": "wait=60"},
    json={"model": "fvimg-2", "file": file["id"]},
)
while analysis["status"] == "processing":
    time.sleep(3)
    analysis = api("GET", f"/analyses/{analysis['id']}")

if analysis["status"] != "succeeded":
    problem = analysis["error"] or {}
    sys.exit(f"The check {analysis['status']}: {problem.get('message')}")
summary = analysis["summary"]
print(f"{analysis['id']} {analysis['status']}")
print(f"level {summary['level']}, action {summary['action']}, assessment {summary['assessment']}")
print(f"AI generation: {summary['ai']}, editing: {summary['edit']}")
print(f"Report: {analysis['report_url']}")
```

**Ausgabe der Bildprüfung**

```text
an_8Hk2pQ4vN9sL1xR7cW3mY6zB succeeded
level L2, action accept, assessment authentic_untouched
AI generation: very_low, editing: none
Report: https://fileverity.com/verify/FV-7K2M-9QXD-4T8R
```

`summary` ist die stabile, maschinenlesbare Einstufung einer Bildprüfung: `level`, `action`, `assessment`, `ai` und `edit`, beschrieben unter [Zusammenfassung bei Bildern](https://fileverity.com/de/docs/analyses.md#summary-image). `report_url` ist die öffentliche Verifizierungsseite des Berichts. Mit einem Testschlüssel ist das Ergebnis ein festes Beispiel, und hinter seiner Berichts-URL gibt es keine Seite.

## Ein großes PDF prüfen

Dokumente bis 50 MB werden in drei Schritten hochgeladen: Fordern Sie mit `POST /files` und einem JSON-Anfragetext eine Upload-URL an, senden Sie die Bytes mit `PUT` an diese URL und warten Sie, bis die Datei den Status `uploaded` zeigt. Danach erstellt das Programm die Analyse und fragt alle 5 Sekunden ab, weil Dokumente in eine Warteschlange kommen können und eine Prüfung eine Weile dauern kann. Es gibt den Fortschritt, die Zusammenfassung und die Berichts-URL aus und speichert den PDF-Bericht neben dem Skript.

Speichern Sie es als `check-pdf.mjs` oder `check_pdf.py` und führen Sie `node check-pdf.mjs contract.pdf` oder `python check_pdf.py contract.pdf` aus. Für einen anderen Dokumenttyp ändern Sie `content_type` auf einen der [akzeptierten Content-Types](https://fileverity.com/de/docs/files.md#content-types). Für ein verschlüsseltes Dokument fügen Sie der Analyseanfrage `options: { password }` hinzu.

**Ein großes PDF prüfen: JavaScript**

```javascript
import { randomUUID } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { basename } from "node:path";

const base = "https://api.fileverity.com/v1";
const key = process.env.FILEVERITY_API_KEY;
const path = process.argv[2];
if (!key || !path) throw new Error("Usage: FILEVERITY_API_KEY=... node check-pdf.mjs contract.pdf");

const retryable = ["rate_limited", "idempotency_in_progress"];
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function request(url, init) {
  for (let attempt = 1; ; attempt++) {
    let response = null;
    let problem;
    try {
      response = await fetch(url, init);
      if (response.ok) {
        const remaining = response.headers.get("x-ratelimit-remaining-requests");
        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));
        if (remaining === "0") await sleep(reset);
        return response;
      }
      problem = (await response.json().catch(() => ({}))).error ?? {};
    } catch (error) {
      problem = { code: "network_error", message: error.message };
    }
    const status = response?.status ?? 0;
    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);
    if (!retry || attempt === 5) {
      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;
      throw new Error(`${code}: ${message} (request ${id})`);
    }
    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);
  }
}

async function api(method, route, { headers, body } = {}) {
  const auth = { Authorization: `Bearer ${key}` };
  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });
  return response.json();
}

const bytes = await readFile(path);
const file = await api("POST", "/files", {
  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
  body: JSON.stringify({
    filename: basename(path),
    content_type: "application/pdf",
    bytes: bytes.length,
  }),
});

if (file.status === "pending") {
  const { url, method, headers } = file.upload;
  await request(url, { method, headers, body: bytes });
  let ready = false;
  for (let attempt = 0; attempt < 15 && !ready; attempt++) {
    ready = (await api("GET", `/files/${file.id}`)).status === "uploaded";
    if (!ready) await sleep(2);
  }
  if (!ready) throw new Error("The upload was not confirmed.");
}
console.log(`${file.id} uploaded`);

let analysis = await api("POST", "/analyses", {
  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
  body: JSON.stringify({ model: "fvdoc-3", file: file.id }),
});
const deadline = Date.now() + 15 * 60 * 1000;
while (analysis.status === "processing") {
  if (Date.now() > deadline) {
    throw new Error(`Still processing. Fetch it later with GET /analyses/${analysis.id}`);
  }
  console.log(`${analysis.id} processing: ${analysis.progress?.stage ?? "starting"}`);
  await sleep(5);
  analysis = await api("GET", `/analyses/${analysis.id}`);
}

if (analysis.status !== "succeeded") {
  throw new Error(`The check ${analysis.status}: ${analysis.error?.message}`);
}
const { level, action, reason, pages, images, text, findings } = analysis.summary;
console.log(`${analysis.id} ${analysis.status}`);
console.log(`level ${level}, action ${action}, reason ${reason ?? "none"}`);
console.log(`${pages} pages, ${images} photos examined, text: ${text ?? "not evaluated"}`);
console.log(`findings: ${findings.join(", ") || "none"}`);
console.log(`Report: ${analysis.report_url}`);

if (analysis.livemode) {
  const pdf = await request(`${base}/analyses/${analysis.id}/report.pdf`, {
    headers: { Authorization: `Bearer ${key}` },
  });
  const name = `Fileverity-${analysis.report_id}.pdf`;
  await writeFile(name, Buffer.from(await pdf.arrayBuffer()));
  console.log(`Saved ${name}`);
}
```

**Ein großes PDF prüfen: Python**

```python
import os
import sys
import time
import uuid

import requests

BASE = "https://api.fileverity.com/v1"
KEY = os.environ.get("FILEVERITY_API_KEY")
if not KEY or len(sys.argv) < 2:
    sys.exit("Usage: FILEVERITY_API_KEY=... python check_pdf.py contract.pdf")

RETRYABLE = ("rate_limited", "idempotency_in_progress")


def request(method, url, **kwargs):
    for attempt in range(1, 6):
        try:
            response = requests.request(method, url, timeout=90, **kwargs)
        except requests.RequestException as error:
            response, problem = None, {"code": "network_error", "message": str(error)}
        else:
            if response.ok:
                if response.headers.get("x-ratelimit-remaining-requests") == "0":
                    time.sleep(int(response.headers["x-ratelimit-reset-requests"]))
                return response
            try:
                problem = response.json()["error"]
            except (ValueError, KeyError, TypeError):
                problem = {}
        status = response.status_code if response is not None else 0
        retry = status == 0 or status >= 500 or problem.get("code") in RETRYABLE
        if not retry or attempt == 5:
            code = problem.get("code", status)
            message = problem.get("message", "Request failed")
            request_id = problem.get("request_id", "unknown")
            raise RuntimeError(f"{code}: {message} (request {request_id})")
        wait = response.headers.get("retry-after") if response is not None else None
        time.sleep(int(wait or 2**attempt))


def api(method, route, headers=None, **kwargs):
    headers = {"Authorization": f"Bearer {KEY}", **(headers or {})}
    return request(method, BASE + route, headers=headers, **kwargs).json()


with open(sys.argv[1], "rb") as source:
    data = source.read()
file = api(
    "POST",
    "/files",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={
        "filename": os.path.basename(sys.argv[1]),
        "content_type": "application/pdf",
        "bytes": len(data),
    },
)

if file["status"] == "pending":
    request("PUT", file["upload"]["url"], headers=file["upload"]["headers"], data=data)
    for _ in range(15):
        if api("GET", f"/files/{file['id']}")["status"] == "uploaded":
            break
        time.sleep(2)
    else:
        sys.exit("The upload was not confirmed.")
print(f"{file['id']} uploaded")

analysis = api(
    "POST",
    "/analyses",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={"model": "fvdoc-3", "file": file["id"]},
)
deadline = time.time() + 15 * 60
while analysis["status"] == "processing":
    if time.time() > deadline:
        sys.exit(f"Still processing. Fetch it later with GET /analyses/{analysis['id']}")
    stage = (analysis["progress"] or {}).get("stage", "starting")
    print(f"{analysis['id']} processing: {stage}")
    time.sleep(5)
    analysis = api("GET", f"/analyses/{analysis['id']}")

if analysis["status"] != "succeeded":
    problem = analysis["error"] or {}
    sys.exit(f"The check {analysis['status']}: {problem.get('message')}")
summary = analysis["summary"]
reason = summary["reason"] or "none"
text = summary["text"] or "not evaluated"
findings = ", ".join(summary["findings"]) or "none"
print(f"{analysis['id']} {analysis['status']}")
print(f"level {summary['level']}, action {summary['action']}, reason {reason}")
print(f"{summary['pages']} pages, {summary['images']} photos examined, text: {text}")
print(f"findings: {findings}")
print(f"Report: {analysis['report_url']}")

if analysis["livemode"]:
    url = f"{BASE}/analyses/{analysis['id']}/report.pdf"
    pdf = request("GET", url, headers={"Authorization": f"Bearer {KEY}"})
    name = f"Fileverity-{analysis['report_id']}.pdf"
    with open(name, "wb") as target:
        target.write(pdf.content)
    print(f"Saved {name}")
```

**Ausgabe der PDF-Prüfung**

```text
file_Pw3mY6zB8Hk2pQ4vN9sL1xR7 uploaded
an_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: queued
an_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: analyzing
an_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: reporting
an_Zc4vB7nP1sK8tL3wR6yQm2Xa succeeded
level L2, action accept, reason clean
12 pages, 3 photos examined, text: human
findings: none
Report: https://fileverity.com/verify/FV-3PXH-6WQ2-8NDC
Saved Fileverity-FV-3PXH-6WQ2-8NDC.pdf
```

- Das `PUT` geht direkt an den Speicher. Es braucht keinen Header `Authorization`, und die Header in `upload.headers` werden genau so gesendet, wie sie zurückgegeben wurden. Siehe [Upload über vorsignierte URL](https://fileverity.com/de/docs/files.md#presigned).
- Mit einem Testschlüssel kommt die Datei bereits als `uploaded` zurück, und es gibt nichts zu senden, daher überspringt das Programm den Upload. Die Analyse liefert sofort ein festes Beispiel, und es gibt kein Beispiel-PDF, daher speichert das Programm den Bericht nur bei einem Live-Schlüssel.
- Laden Sie das PDF herunter, sobald die Prüfung abgeschlossen ist. Die Seitenbilder und Textauszüge eines Dokuments werden 3 Stunden lang aufbewahrt, und ein später heruntergeladenes PDF enthält sie nicht mehr. Siehe [PDF-Bericht](https://fileverity.com/de/docs/analyses.md#pdf).
- Eine Prüfung, die mit `failed` oder `canceled` endet, wurde nicht berechnet. Das Programm bricht mit der Fehlermeldung der Analyse ab.

## Von KI verfassten Text erkennen

Die Textprüfung braucht nur einen einzigen synchronen Aufruf. Das Programm liest eine Textdatei, sendet sie an `POST /text/detections` und gibt die Einstufung, den Score, den Anteil des von KI verfassten Textes und jede von KI verfasste Passage mit ihren Offsets aus. Die Erkennung von Texten, die von KI verfasst wurden, unterstützt Englisch, Französisch, Deutsch, Spanisch und Chinesisch (`en`, `fr`, `de`, `es`, `zh`). Die Sprache wird automatisch erkannt; übergeben Sie einen Code als zweites Argument, um sie festzulegen. Der Text muss mindestens 100 Wörter und darf höchstens 100.000 Zeichen umfassen.

Speichern Sie es als `detect-text.mjs` oder `detect_text.py` und führen Sie `node detect-text.mjs essay.txt` oder `python detect_text.py essay.txt fr` aus. Der [Schnellstart](https://fileverity.com/de/docs/quickstart.md#text) enthält einen Befehl, der eine Beispieldatei `essay.txt` erstellt. Mit einem Testschlüssel ist die Antwort ein festes Ergebnis für einen von Menschen verfassten Text, sodass das Programm keine Passage ausgibt.

**Von KI verfassten Text erkennen: JavaScript**

```javascript
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";

const base = "https://api.fileverity.com/v1";
const key = process.env.FILEVERITY_API_KEY;
const [, , path, language] = process.argv;
if (!key || !path) {
  throw new Error("Usage: FILEVERITY_API_KEY=... node detect-text.mjs essay.txt [en|fr|de|es|zh]");
}

const retryable = ["rate_limited", "idempotency_in_progress"];
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function request(url, init) {
  for (let attempt = 1; ; attempt++) {
    let response = null;
    let problem;
    try {
      response = await fetch(url, init);
      if (response.ok) {
        const remaining = response.headers.get("x-ratelimit-remaining-requests");
        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));
        if (remaining === "0") await sleep(reset);
        return response;
      }
      problem = (await response.json().catch(() => ({}))).error ?? {};
    } catch (error) {
      problem = { code: "network_error", message: error.message };
    }
    const status = response?.status ?? 0;
    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);
    if (!retry || attempt === 5) {
      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;
      throw new Error(`${code}: ${message} (request ${id})`);
    }
    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);
  }
}

async function api(method, route, { headers, body } = {}) {
  const auth = { Authorization: `Bearer ${key}` };
  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });
  return response.json();
}

const text = await readFile(path, "utf8");
const detection = await api("POST", "/text/detections", {
  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
  body: JSON.stringify({ text, language }),
});

const { verdict, score, ai_fraction: share, usage, segments } = detection;
console.log(`verdict: ${verdict} (score ${score.toFixed(2)})`);
console.log(`AI-written share: ${Math.round(share * 100)}% of ${usage.characters} characters`);
console.log(`language: ${detection.language}, billed units: ${usage.billed_units}`);
const points = Array.from(text);
for (const { start, end, score: passage } of segments) {
  const excerpt = points.slice(start, Math.min(end, start + 80)).join("");
  console.log(`${start}-${end} score ${passage.toFixed(2)}: ${excerpt}`);
}
```

**Von KI verfassten Text erkennen: Python**

```python
import os
import sys
import time
import uuid

import requests

BASE = "https://api.fileverity.com/v1"
KEY = os.environ.get("FILEVERITY_API_KEY")
if not KEY or len(sys.argv) < 2:
    sys.exit("Usage: FILEVERITY_API_KEY=... python detect_text.py essay.txt [en|fr|de|es|zh]")
sys.stdout.reconfigure(encoding="utf-8")

RETRYABLE = ("rate_limited", "idempotency_in_progress")


def request(method, url, **kwargs):
    for attempt in range(1, 6):
        try:
            response = requests.request(method, url, timeout=90, **kwargs)
        except requests.RequestException as error:
            response, problem = None, {"code": "network_error", "message": str(error)}
        else:
            if response.ok:
                if response.headers.get("x-ratelimit-remaining-requests") == "0":
                    time.sleep(int(response.headers["x-ratelimit-reset-requests"]))
                return response
            try:
                problem = response.json()["error"]
            except (ValueError, KeyError, TypeError):
                problem = {}
        status = response.status_code if response is not None else 0
        retry = status == 0 or status >= 500 or problem.get("code") in RETRYABLE
        if not retry or attempt == 5:
            code = problem.get("code", status)
            message = problem.get("message", "Request failed")
            request_id = problem.get("request_id", "unknown")
            raise RuntimeError(f"{code}: {message} (request {request_id})")
        wait = response.headers.get("retry-after") if response is not None else None
        time.sleep(int(wait or 2**attempt))


def api(method, route, headers=None, **kwargs):
    headers = {"Authorization": f"Bearer {KEY}", **(headers or {})}
    return request(method, BASE + route, headers=headers, **kwargs).json()


with open(sys.argv[1], encoding="utf-8") as source:
    text = source.read()
language = sys.argv[2] if len(sys.argv) > 2 else None
detection = api(
    "POST",
    "/text/detections",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={"text": text, "language": language},
)

usage = detection["usage"]
share = round(detection["ai_fraction"] * 100)
print(f"verdict: {detection['verdict']} (score {detection['score']:.2f})")
print(f"AI-written share: {share}% of {usage['characters']} characters")
print(f"language: {detection['language']}, billed units: {usage['billed_units']}")
for segment in detection["segments"]:
    start, end = segment["start"], segment["end"]
    excerpt = text[start : min(end, start + 80)]
    print(f"{start}-{end} score {segment['score']:.2f}: {excerpt}")
```

**Ausgabe für das Beispiel aus dem Schnellstart**

```text
verdict: mixed (score 0.97)
AI-written share: 50% of 889 characters
language: en, billed units: 1
267-495 score 0.97: In today's fast-paced logistics landscape, it is essential for businesses to pri
590-809 score 0.93: Moving forward, it is crucial to implement robust quality assurance measures and
```

`start` und `end` zählen Unicode-Codepoints, nicht Bytes und nicht UTF-16-Einheiten. Python-Strings werden bereits nach Codepoints indiziert; das Node.js-Programm zerlegt den Text zuerst mit `Array.from`. Siehe [Segment-Offsets](https://fileverity.com/de/docs/text-detections.md#offsets).

> **Hinweis.** Die Textprüfung liefert eine statistische Schätzung, keinen Nachweis der Urheberschaft. Nutzen Sie sie als einen Anhaltspunkt für eine Prüfung durch Menschen und stützen Sie Entscheidungen, die eine Person betreffen, nicht allein darauf.

## Webhooks empfangen und Signaturen prüfen

Ein Webhook teilt Ihrem Server mit, wann eine Prüfung abgeschlossen ist, sodass Sie nicht abfragen müssen. Dieser Empfänger ist ein minimaler HTTP-Server: das Modul `http` von Node.js und Flask für Python. Er prüft die Signatur jeder Zustellung, ignoriert eine Zustellung, die er bereits verarbeitet hat, und gibt das Ereignis aus. Er folgt der Seite [Webhooks](https://fileverity.com/de/docs/webhooks.md#signature).

1. Machen Sie Port 3000 unter einer öffentlichen HTTPS-Adresse erreichbar, zum Beispiel mit einem Tunnel zu Ihrem Rechner. Endpunkt-URLs müssen HTTPS am Standardport 443 verwenden und auf eine öffentliche Adresse zeigen.
2. Öffnen Sie in der Konsole **Webhooks**, fügen Sie einen Endpunkt mit der URL `https://your-host/webhooks/fileverity` hinzu und wählen Sie seine Ereignisse. Kopieren Sie das Signing-Secret, das mit `whsec_` beginnt. Es wird nur einmal angezeigt.
3. Speichern Sie das Programm als `webhook-receiver.mjs` oder `webhook_receiver.py` und starten Sie es mit dem Secret: `FILEVERITY_WEBHOOK_SECRET=whsec_... node webhook-receiver.mjs` oder `FILEVERITY_WEBHOOK_SECRET=whsec_... python webhook_receiver.py`.
4. Wählen Sie beim Endpunkt **Test senden**. Testschlüssel senden nie Ereignisse; **Test senden** stellt stattdessen ein signiertes Beispielereignis zu.

**Webhooks empfangen: JavaScript**

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";

const secret = process.env.FILEVERITY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set FILEVERITY_WEBHOOK_SECRET to the endpoint's signing secret.");
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const port = Number(process.env.PORT) || 3000;
const seen = new Set();

function verify(headers, body) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !timestamp || !signatures) return false;
  if (!(Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300)) return false;
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.`).update(body).digest();
  return signatures.split(" ").some((entry) => {
    const [version, value = ""] = entry.split(",");
    const given = Buffer.from(value, "base64");
    return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
  });
}

function handleEvent(event) {
  const { type, data } = event;
  if (data.test) {
    console.log(`Test event ${event.id}`);
  } else if (type === "analysis.completed") {
    const { level, action } = data.summary;
    console.log(`${data.id} succeeded: level ${level}, action ${action}, ${data.report_url}`);
  } else if (type === "analysis.failed") {
    console.log(`${data.id} ${data.status}: ${data.error.code}`);
  } else if (type === "balance.low") {
    const money = (cents) => `US$${(cents / 100).toFixed(2)}`;
    console.log(`Balance ${money(data.balance)} is below your ${money(data.threshold)} alert`);
  }
}

async function receive(req, res) {
  if (req.method !== "POST" || req.url !== "/webhooks/fileverity") return res.writeHead(404).end();
  const chunks = [];
  let size = 0;
  for await (const chunk of req) {
    size += chunk.length;
    if (size > 1_000_000) return res.writeHead(413).end();
    chunks.push(chunk);
  }
  const body = Buffer.concat(chunks);
  if (!verify(req.headers, body)) return res.writeHead(400).end();
  const event = JSON.parse(body.toString("utf8"));
  try {
    if (!seen.has(event.id)) {
      handleEvent(event);
      seen.add(event.id);
    }
  } catch (error) {
    console.error(`Could not handle ${event.id}`, error);
    return res.writeHead(500).end();
  }
  res.writeHead(200).end();
}

createServer((req, res) => receive(req, res).catch(() => res.destroy())).listen(port, () => console.log(`Listening on port ${port}`));
```

**Webhooks empfangen: Python**

```python
import base64
import hashlib
import hmac
import json
import os
import threading
import time

from flask import Flask, request

SECRET = os.environ.get("FILEVERITY_WEBHOOK_SECRET")
if not SECRET:
    raise SystemExit("Set FILEVERITY_WEBHOOK_SECRET to the endpoint's signing secret.")
KEY = base64.b64decode(SECRET.removeprefix("whsec_"))
seen = set()
lock = threading.Lock()
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1_000_000


def verify(headers, body):
    message_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp", "")
    signatures = headers.get("webhook-signature", "")
    if not message_id or not timestamp.isdecimal() or abs(time.time() - int(timestamp)) > 300:
        return False
    signed = f"{message_id}.{timestamp}.".encode() + body
    expected = hmac.new(KEY, signed, hashlib.sha256).digest()
    for entry in signatures.split(" "):
        version, _, value = entry.partition(",")
        try:
            given = base64.b64decode(value, validate=True)
        except ValueError:
            continue
        if version == "v1" and hmac.compare_digest(given, expected):
            return True
    return False


def handle_event(event):
    data = event["data"]
    if data.get("test"):
        print(f"Test event {event['id']}")
    elif event["type"] == "analysis.completed":
        level, action = data["summary"]["level"], data["summary"]["action"]
        print(f"{data['id']} succeeded: level {level}, action {action}, {data['report_url']}")
    elif event["type"] == "analysis.failed":
        print(f"{data['id']} {data['status']}: {data['error']['code']}")
    elif event["type"] == "balance.low":
        balance, threshold = data["balance"] / 100, data["threshold"] / 100
        print(f"Balance US${balance:.2f} is below your US${threshold:.2f} alert")


@app.post("/webhooks/fileverity")
def receive():
    body = request.get_data()
    if not verify(request.headers, body):
        return "", 400
    event = json.loads(body)
    with lock:
        try:
            if event["id"] not in seen:
                handle_event(event)
                seen.add(event["id"])
        except Exception:
            app.logger.exception("Could not handle %s", event["id"])
            return "", 500
    return "", 200


if __name__ == "__main__":
    port = int(os.environ.get("PORT", "3000"))
    print(f"Listening on port {port}")
    app.run(port=port)
```

**Ausgabe des Empfängers**

```text
Listening on port 3000
Test event evt_Qm4vB7nP1sK8tL3wR6yZ2cXa
an_8Hk2pQ4vN9sL1xR7cW3mY6zB succeeded: level L2, action accept, https://fileverity.com/verify/FV-7K2M-9QXD-4T8R
```

- **Unveränderter Anfragetext.** Die Signatur umfasst genau die gesendeten Bytes. Die Programme lesen den Anfragetext als Bytes, berechnen einen HMAC-SHA256 über `webhook-id`, einen Punkt, `webhook-timestamp`, einen Punkt und diese Bytes, mit dem Base64-dekodierten Secret als Schlüssel, und parsen das JSON erst, nachdem die Signatur als gültig bestätigt wurde.
- **Vergleich in konstanter Zeit.** `timingSafeEqual` in Node.js und `hmac.compare_digest` in Python vergleichen das Ergebnis mit jeder `v1`-Signatur im Header `webhook-signature`.
- **Zeitstempel-Toleranz.** Ein `webhook-timestamp`, der mehr als fünf Minuten von der Systemzeit abweicht oder keine Zahl ist, wird abgelehnt.
- **Schnelle Antworten.** Eine gültige Zustellung erhält `200`, sobald das Ereignis verarbeitet ist. Bei einer ungültigen Signatur lautet die Antwort `400`. Schlägt die Verarbeitung fehl, antwortet das Programm mit `500`, und Fileverity wiederholt die Zustellung später mit derselben Ereignis-ID. Siehe [Zustellung und Wiederholungen](https://fileverity.com/de/docs/webhooks.md#delivery).
- **Duplikate.** Die Zustellung erfolgt mindestens einmal, und eine Wiederholung behält die Ereignis-ID. Die Programme merken sich die IDs, die sie verarbeitet haben, und antworten mit `200`, ohne ein Ereignis zweimal zu verarbeiten. Ersetzen Sie im Produktivbetrieb das Set im Arbeitsspeicher durch einen Unique Constraint auf die Ereignis-ID in Ihrer Datenbank und stellen Sie die Arbeit in eine Warteschlange, damit die Antwort schnell bleibt.
- **Ereignisdaten.** `analysis.completed` und `analysis.failed` enthalten die Analyse ohne `result`; rufen Sie `GET /analyses/{id}` auf, wenn Sie den vollständigen Bericht brauchen. `balance` und `threshold` in `balance.low` sind in US-Cent angegeben.

## Wie es weitergeht

- Einen vollständigen Eingangs-Workflow mit Prüfung durch Menschen finden Sie unter [Dokumentprüfungen in einem Eingangs-Workflow](https://fileverity.com/de/docs/intake-workflow.md).
- Schlagen Sie jedes Feld in der [API-Referenz](https://fileverity.com/de/docs/api-reference.md) nach, oder laden Sie das OpenAPI-Dokument in Ihre eigenen Tools.
- Gehen Sie mit einem Live-Schlüssel, einem monatlichen Ausgabenlimit und einem `balance.low`-Webhook live. Siehe [Preise und Abrechnung](https://fileverity.com/de/docs/pricing.md#alerts).
- Brauchen Sie Hilfe? Geben Sie den Wert des Headers `x-request-id` an, wenn Sie den [Support kontaktieren](https://fileverity.com/de/kontakt).

