Chuyển đến nội dung chính

Tài liệu API Fileverity

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.

Xem dạng MarkdownCập nhật 2026-11-04

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, Lượt phân tích, Phát hiện văn bản và Webhook.

Chương trìnhChức năngLệnh gọi
Kiểm tra ảnhTả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ớnTả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ếtGử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 webhookXá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 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.
export 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.
IdempotencyMỗ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.
LỗiPhả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.
Thử lạiLỗ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.
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.

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.

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}`);
an_8Hk2pQ4vN9sL1xR7cW3mY6zB succeededlevel L2, action accept, assessment authentic_untouchedAI generation: very_low, editing: noneReport: 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. 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. 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.

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}`);}
file_Pw3mY6zB8Hk2pQ4vN9sL1xR7 uploadedan_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: queuedan_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: analyzingan_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: reportingan_Zc4vB7nP1sK8tL3wR6yQm2Xa succeededlevel L2, action accept, reason clean12 pages, 3 photos examined, text: humanfindings: noneReport: https://fileverity.com/verify/FV-3PXH-6WQ2-8NDCSaved 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.
  • 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.
  • 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 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.

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}`);}
verdict: mixed (score 0.97)AI-written share: 50% of 889 characterslanguage: en, billed units: 1267-495 score 0.97: In today's fast-paced logistics landscape, it is essential for businesses to pri590-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.

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.

  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ý.
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}`));
Listening on port 3000Test event evt_Qm4vB7nP1sK8tL3wR6yZ2cXaan_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.
  • 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