# Mã mẫu

> Các chương trình Node.js và Python hoàn chỉnh cho Fileverity API: kiểm tra ảnh, kiểm tra tệp PDF lớn, phát hiện văn bản do AI viết và xác minh chữ ký webhook.

Nguồn: https://fileverity.com/vi/docs/sample-code

Trang này có bốn chương trình hoàn chỉnh cho Fileverity API, mỗi chương trình có bản Node.js 18 trở lên và bản Python 3.9 trở lên: kiểm tra ảnh và chờ kết quả, kiểm tra tệp PDF lớn, phát hiện văn bản do AI viết, và nhận webhook kèm xác minh chữ ký. Hãy sao chép một chương trình, đặt `FILEVERITY_API_KEY` rồi chạy.

Các chương trình gọi trực tiếp API HTTPS, dùng `fetch` có sẵn trong Node.js và gói `requests` trong Python, nên bạn chỉ cần một HTTP client. Các lệnh gọi, trường và trạng thái là những gì được mô tả trong [Tệp](https://fileverity.com/vi/docs/files.md), [Lượt phân tích](https://fileverity.com/vi/docs/analyses.md), [Phát hiện văn bản](https://fileverity.com/vi/docs/text-detections.md) và [Webhook](https://fileverity.com/vi/docs/webhooks.md).

| Chương trình | Chức năng | Lệnh gọi |
| --- | --- | --- |
| [Kiểm tra ảnh](https://fileverity.com/vi/docs/sample-code.md#image) | Tải ảnh lên, chờ kết quả rồi in ra phần tóm tắt và URL báo cáo. | `POST /files`, `POST /analyses`, `GET /analyses/{id}` |
| [Kiểm tra tệp PDF lớn](https://fileverity.com/vi/docs/sample-code.md#pdf) | Tải tài liệu lên bằng URL ký trước, truy vấn định kỳ cho đến khi lượt kiểm tra hoàn tất rồi lưu báo cáo PDF. | `POST /files`, `GET /files/{id}`, `POST /analyses`, `GET /analyses/{id}`, `GET /analyses/{id}/report.pdf` |
| [Phát hiện văn bản do AI viết](https://fileverity.com/vi/docs/sample-code.md#text) | Gửi một tệp văn bản rồi in ra kết luận, điểm số và từng đoạn văn do AI viết. | `POST /text/detections` |
| [Nhận webhook](https://fileverity.com/vi/docs/sample-code.md#webhooks) | Xác minh chữ ký của từng lần gửi, bỏ qua các yêu cầu trùng lặp và xử lý các sự kiện. | Endpoint của bạn, được gọi với `analysis.completed`, `analysis.failed` và `balance.low` |

## Trước khi chạy

- Tạo khóa **Thử nghiệm** tại mục [Khóa API](https://fileverity.com/console/keys?lang=vi) trong Console. Khóa thử nghiệm trả về kết quả mẫu cố định và không bao giờ bị tính phí, nên mọi chương trình đều chạy miễn phí. Để kiểm tra tệp thật, hãy tạo khóa **Thật**. Các chương trình không cần thay đổi.
- Node.js: lưu mỗi chương trình thành tệp `.mjs` để có thể dùng `import` và `await` ở cấp cao nhất (top-level), rồi chạy bằng `node`. Không cần cài đặt gì.
- Python: chạy `pip install requests`. Chương trình nhận webhook cần thêm `pip install flask`.

**Đặt khóa: Shell**

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

**Đặt khóa: PowerShell**

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

## Cách các chương trình xử lý khóa, lỗi và thử lại

Ba chương trình gọi API dùng chung các hàm `request` và `api`. Hãy sao chép nguyên các hàm này vào mã của bạn.

| Vấn đề | Cách chương trình xử lý |
| --- | --- |
| Khóa | Được đọc từ `FILEVERITY_API_KEY` và không bao giờ ghi vào mã. Hãy gọi API từ máy chủ của bạn, không gọi từ mã chạy trong trình duyệt. |
| Idempotency | Mỗi yêu cầu POST đều gửi một `Idempotency-Key`, là một UUID mới cho mỗi yêu cầu, và dùng lại khóa đó ở mọi lần thử lại, nên một lần thử lại không bao giờ chạy hoặc tính phí một lượt kiểm tra hai lần. Để việc chạy lại toàn bộ script vẫn an toàn, hãy tạo khóa từ mã bản ghi của chính bạn. Xem [Idempotency](https://fileverity.com/vi/docs/idempotency.md). |
| Lỗi | Phản hồi không phải `2xx` có kèm `error.type`, `error.code`, `error.message`, `error.param` và `error.request_id`. Các chương trình dừng với thông báo `code: message (request request_id)`. Hãy phân nhánh xử lý theo `code` và cung cấp mã yêu cầu khi liên hệ hỗ trợ. Xem [Lỗi](https://fileverity.com/vi/docs/errors.md). |
| Thử lại | Lỗi mạng, mã trạng thái `5xx` (`internal_error`, `analysis_failed`, `service_unavailable`), `rate_limited` (429) và `idempotency_in_progress` (409) đều được thử lại, tối đa 5 lần. Thời gian chờ là giá trị header `retry-after` nếu phản hồi có header này, nếu không thì lần lượt là 2, 4, 8 và 16 giây. Mọi mã `4xx` khác dừng ngay. `pending_files_limit` cũng là mã 429 nhưng không có `retry-after`, nên không được thử lại. |
| Giới hạn tốc độ | Khi một phản hồi báo `x-ratelimit-remaining-requests: 0`, chương trình chờ `x-ratelimit-reset-requests` giây trước lệnh gọi tiếp theo, để không gặp lỗi 429. Xem [Giới hạn tốc độ và header](https://fileverity.com/vi/docs/rate-limits.md#headers). |
| Hết thời gian chờ | Các chương trình Python hết thời gian chờ sau 90 giây cho mỗi yêu cầu, dài hơn 60 giây mà `Prefer: wait` có thể giữ một yêu cầu. |

## Kiểm tra ảnh và chờ kết quả

Chương trình tải ảnh lên dưới dạng multipart, tạo một lượt phân tích với `Prefer: wait=60` để yêu cầu được giữ cho đến khi có kết quả, rồi in ra phần tóm tắt và URL báo cáo. Nếu thời gian chờ kết thúc trước, API trả về `202` với `status: processing`, và chương trình truy vấn định kỳ `GET /analyses/{id}` mỗi 3 giây. Xem [Chờ, truy vấn định kỳ hoặc dùng webhook](https://fileverity.com/vi/docs/analyses.md#wait).

Lưu thành `check-image.mjs` hoặc `check_image.py` rồi chạy `node check-image.mjs photo.jpg` hoặc `python check_image.py photo.jpg`. Multipart nhận ảnh tối đa 4 MB. Với ảnh lớn hơn, tối đa 10 MB, hãy dùng cách tải lên bằng URL ký trước trong chương trình tiếp theo, với content type của ảnh, ví dụ `image/jpeg`.

**Kiểm tra ảnh: 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}`);
```

**Kiểm tra ảnh: 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']}")
```

**Đầu ra của lượt kiểm tra ảnh**

```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` là kết luận ổn định, dạng máy đọc được của một lượt kiểm tra ảnh, gồm `level`, `action`, `assessment`, `ai` và `edit`, được mô tả trong [Tóm tắt ảnh](https://fileverity.com/vi/docs/analyses.md#summary-image). `report_url` là trang xác minh công khai của báo cáo. Với khóa thử nghiệm, kết quả là một mẫu cố định, và URL báo cáo của nó không dẫn đến trang nào.

## Kiểm tra tệp PDF lớn

Tài liệu có dung lượng tối đa 50 MB được tải lên qua ba bước: yêu cầu một URL tải lên bằng `POST /files` với nội dung JSON, gửi các byte đến URL đó bằng `PUT`, và chờ cho đến khi tệp hiển thị `uploaded`. Sau đó chương trình tạo lượt phân tích và truy vấn định kỳ mỗi 5 giây, vì tài liệu có thể phải xếp hàng chờ và một lượt kiểm tra có thể mất một lúc. Chương trình in ra tiến độ, phần tóm tắt và URL báo cáo, rồi lưu báo cáo PDF bên cạnh script.

Lưu thành `check-pdf.mjs` hoặc `check_pdf.py` rồi chạy `node check-pdf.mjs contract.pdf` hoặc `python check_pdf.py contract.pdf`. Với loại tài liệu khác, hãy đổi `content_type` thành một trong các [content type được chấp nhận](https://fileverity.com/vi/docs/files.md#content-types). Với tài liệu được mã hóa, hãy thêm `options: { password }` vào yêu cầu phân tích.

**Kiểm tra tệp PDF lớn: 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}`);
}
```

**Kiểm tra tệp PDF lớn: 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}")
```

**Đầu ra của lượt kiểm tra PDF**

```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
```

- Yêu cầu `PUT` đi thẳng đến kho lưu trữ. Yêu cầu này không dùng header `Authorization`, và các header trong `upload.headers` được gửi đúng như khi được trả về. Xem [Tải lên bằng URL ký trước](https://fileverity.com/vi/docs/files.md#presigned).
- Với khóa thử nghiệm, tệp trả về ở trạng thái `uploaded` và không có gì để gửi, nên chương trình bỏ qua bước tải lên. Lượt phân tích trả về ngay một mẫu cố định, và không có PDF mẫu, nên chương trình chỉ lưu báo cáo khi dùng khóa thật.
- Hãy tải PDF ngay khi lượt kiểm tra hoàn tất. Ảnh các trang và đoạn trích văn bản của tài liệu chỉ được giữ trong 3 giờ, và PDF tải xuống sau đó sẽ không có các nội dung này. Xem [Báo cáo PDF](https://fileverity.com/vi/docs/analyses.md#pdf).
- Lượt kiểm tra kết thúc với `failed` hoặc `canceled` không bị tính phí. Chương trình dừng kèm thông báo lỗi của lượt phân tích.

## Phát hiện văn bản do AI viết

Phát hiện văn bản chỉ cần một lệnh gọi đồng bộ. Chương trình đọc một tệp văn bản, gửi đến `POST /text/detections` rồi in ra kết luận, điểm số, tỷ lệ văn bản do AI viết và từng đoạn văn do AI viết kèm offset (vị trí ký tự). Việc phát hiện văn bản do AI viết hỗ trợ tiếng Anh, tiếng Pháp, tiếng Đức, tiếng Tây Ban Nha và tiếng Trung (`en`, `fr`, `de`, `es`, `zh`). Ngôn ngữ được nhận diện tự động; truyền một mã ngôn ngữ làm đối số thứ hai để chỉ định. Văn bản phải có ít nhất 100 từ và tối đa 100.000 ký tự.

Lưu thành `detect-text.mjs` hoặc `detect_text.py` rồi chạy `node detect-text.mjs essay.txt` hoặc `python detect_text.py essay.txt fr`. Phần [Bắt đầu nhanh](https://fileverity.com/vi/docs/quickstart.md#text) có một lệnh tạo tệp `essay.txt` mẫu. Với khóa thử nghiệm, phản hồi là một kết quả cố định cho văn bản do người viết, nên chương trình không in ra đoạn văn nào.

**Phát hiện văn bản do AI viết: 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}`);
}
```

**Phát hiện văn bản do AI viết: 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}")
```

**Đầu ra cho mẫu trong phần Bắt đầu nhanh**

```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` và `end` đếm theo code point Unicode, không phải byte và không phải đơn vị UTF-16. Chuỗi Python vốn đã đánh chỉ mục theo code point; chương trình Node.js tách văn bản bằng `Array.from` trước. Xem [Offset của đoạn](https://fileverity.com/vi/docs/text-detections.md#offsets).

> **Lưu ý.** Phát hiện văn bản trả về một ước tính thống kê, không phải bằng chứng xác định ai là người viết. Hãy dùng kết quả như một căn cứ cho việc xem xét của con người, và đừng chỉ dựa vào đó để đưa ra quyết định ảnh hưởng đến một người.

## Nhận webhook và xác minh chữ ký

Webhook cho máy chủ của bạn biết khi một lượt kiểm tra hoàn tất, nên bạn không phải truy vấn định kỳ. Chương trình nhận này là một máy chủ HTTP tối giản: mô-đun `http` của Node.js và Flask cho Python. Chương trình xác minh chữ ký của mọi lần gửi, bỏ qua lần gửi đã xử lý rồi và in ra sự kiện. Chương trình làm theo hướng dẫn trên trang [Webhook](https://fileverity.com/vi/docs/webhooks.md#signature).

1. Hãy làm cho cổng 3000 truy cập được tại một địa chỉ HTTPS công khai, ví dụ bằng một tunnel đến máy của bạn. URL endpoint phải dùng HTTPS trên cổng mặc định 443 và trỏ đến một địa chỉ công khai.
2. Trong Console, mở **Webhook**, thêm một endpoint với URL `https://your-host/webhooks/fileverity` và chọn các sự kiện cho endpoint. Sao chép khóa bí mật dùng để ký, bắt đầu bằng `whsec_`. Khóa chỉ hiển thị một lần.
3. Lưu chương trình thành `webhook-receiver.mjs` hoặc `webhook_receiver.py` và khởi động với khóa bí mật: `FILEVERITY_WEBHOOK_SECRET=whsec_... node webhook-receiver.mjs` hoặc `FILEVERITY_WEBHOOK_SECRET=whsec_... python webhook_receiver.py`.
4. Chọn **Gửi thử** trên endpoint. Khóa thử nghiệm không bao giờ gửi sự kiện; thay vào đó, **Gửi thử** gửi một sự kiện mẫu đã được ký.

**Nhận webhook: 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}`));
```

**Nhận webhook: 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)
```

**Đầu ra của chương trình nhận**

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

- **Nội dung yêu cầu thô (raw body).** Chữ ký được tính trên đúng các byte đã được gửi. Các chương trình đọc nội dung yêu cầu dưới dạng byte, tính HMAC-SHA256 trên `webhook-id`, một dấu chấm, `webhook-timestamp`, một dấu chấm và các byte đó, với khóa bí mật đã giải mã base64 làm khóa, và chỉ phân tích JSON sau khi chữ ký hợp lệ.
- **So sánh trong thời gian không đổi.** `timingSafeEqual` trong Node.js và `hmac.compare_digest` trong Python so sánh kết quả với mọi chữ ký `v1` trong header `webhook-signature`.
- **Độ lệch thời gian cho phép.** `webhook-timestamp` lệch hơn năm phút so với đồng hồ, hoặc không phải là số, sẽ bị từ chối.
- **Phản hồi nhanh.** Lần gửi hợp lệ nhận `200` ngay khi sự kiện được xử lý. Chữ ký sai nhận `400`. Nếu việc xử lý thất bại, chương trình trả về `500`, và Fileverity sẽ thử lại sau với cùng mã sự kiện. Xem [Gửi sự kiện và thử lại](https://fileverity.com/vi/docs/webhooks.md#delivery).
- **Trùng lặp.** Mỗi sự kiện được gửi ít nhất một lần, và lần thử lại giữ nguyên mã sự kiện. Các chương trình ghi nhớ các mã đã xử lý và trả về `200` mà không xử lý một sự kiện hai lần. Trong môi trường production, hãy thay tập hợp trong bộ nhớ bằng ràng buộc duy nhất (unique constraint) trên mã sự kiện trong cơ sở dữ liệu của bạn, và đưa công việc vào hàng đợi để phản hồi luôn nhanh.
- **Dữ liệu sự kiện.** `analysis.completed` và `analysis.failed` chứa lượt phân tích nhưng không có `result`; hãy gọi `GET /analyses/{id}` khi bạn cần báo cáo đầy đủ. `balance` và `threshold` trong `balance.low` tính bằng cent Mỹ.

## Bước tiếp theo

- Chạy trọn một quy trình tiếp nhận có người xem xét trong [Kiểm tra tài liệu trong quy trình tiếp nhận hồ sơ](https://fileverity.com/vi/docs/intake-workflow.md).
- Tra cứu bất kỳ trường nào trong [Tham chiếu API](https://fileverity.com/vi/docs/api-reference.md), hoặc nạp tài liệu OpenAPI vào các công cụ của riêng bạn.
- Triển khai chính thức với khóa thật, hạn mức chi tiêu hằng tháng và webhook `balance.low`. Xem [Giá và thanh toán](https://fileverity.com/vi/docs/pricing.md#alerts).
- Cần trợ giúp? Hãy cung cấp giá trị header `x-request-id` khi bạn [liên hệ hỗ trợ](https://fileverity.com/vi/lien-he).

