Documentation · API v1
Developer API
Send a PDF to the Veritas-Doc API and get back the same report of technical signs of modification as in the web interface: as JSON for your integrations, or as an annotated PDF report to file away. Two endpoints, one access key, no SDK to install.
On this page
Introduction
The API lets you build Veritas-Doc analysis into line-of-business software, a document management system or a script: your server sends a PDF and the API replies with the full report, in the same format as the one shown in the interface (status, confidence score, detected editing tool, findings, zones and recovered text).
The result is a report of technical signs of modification, not a verdict. A sign does not prove fraud and does not establish that a document is genuine: a document re-saved for a legitimate reason produces the same signs. Interpreting the report remains the responsibility of the person who reads it.
JSON is the default format, designed for integrations. Optionally, the API returns the report as an annotated PDF: a summary page, then the pages of the original document with the flagged areas outlined, ready to be filed with the case (see Annotated PDF report).
Pass the disclaimer field on to your users
Every response contains a disclaimer field that restates the scope of the report. The text must be passed on, as is, to the end users who see the result in your product.
At a glance
- Base URL
https://veritas-doc.techstride.app/api/v1- Access
- Active Firm subscription, or the Enterprise plan
- Authentication
- Access key in the
Authorization: Bearerheader - Formats
- Request as a raw PDF or
multipart/form-data; response as JSON (default) or as an annotated PDF report (format=pdf) - Usage
- Server-to-server calls only (no CORS headers)
- Version
- v1, announced by the
X-Veritas-API-Version: 1header
Quick start
Three steps are enough to get a first report.
Get an access key
Sign in, open My account and create a key in the API section. Give it a name that identifies what it is used for. The key is shown only once: copy it straight into your secrets manager. API access requires an active Firm subscription or the Enterprise plan (see the plans).
Make a first call
Send a PDF with
curl. To check only that your key works, without using up an analysis, call GET /api/v1/usage first.curl -X POST "https://veritas-doc.techstride.app/api/v1/analyze" \ -H "Authorization: Bearer vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \ -H "Content-Type: application/pdf" \ --data-binary @document.pdfRead the response
The response is the report, as JSON. Read
status,confidence_scoreandfindingsfirst, then passdisclaimeron to your users. Each field is described in the Response section. To get a PDF to file away instead of JSON, add?format=pdf(see Annotated PDF report).
Authentication
Every /api/v1/* route expects an access key in the Authorization header, in Bearer format.
Authorization: Bearer vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXA key starts with vd_live_ and is 51 characters long in total. Veritas-Doc does not keep its text: it is shown only once, when you create it. If you lose it, revoke it and create another one.
The API uses no cookie and no session. It sends no CORS header: it is designed for server-to-server calls, and a key must never be embedded in a web page or in a distributed application.
Access rights are checked on every call.
Authentication errors
| Code | HTTP | Case |
|---|---|---|
MISSING_API_KEY | 401 | Header missing, or malformed key. |
INVALID_API_KEY | 401 | Unknown, revoked or expired key. The message is identical in all three cases, so that nothing is revealed. |
API_ACCESS_NOT_ALLOWED | 403 | The key is valid, but the account has no API access (neither an active Firm subscription nor the Enterprise plan). |
Analyse a PDF
POST/api/v1/analyze
Sends a PDF and returns its report, as JSON (default) or as an annotated PDF report (`format=pdf`). Two body formats are accepted.
Request body
| Format | Content-Type | Body |
|---|---|---|
| Raw PDF | application/pdf | The bytes of the file, as they are. |
| Form | multipart/form-data | A field named file that holds the PDF. |
The include_previews parameter
By default, the image preview of each page (pages[].preview) is removed from the response, to keep it light. Add ?include_previews=true to the URL to keep it, for example POST /api/v1/analyze?include_previews=true. Only ask for it if you display the pages.
The format parameter
?format=json (default) or ?format=pdf. You can also send Accept: application/pdf; if both are present, the parameter wins. Any other value gives 400 BAD_REQUEST, and nothing is counted. The PDF report is described in Annotated PDF report.
Limits
- Size: documents of any size are accepted. Beyond 4.5 MB, the current platform may refuse the upload before it reaches the API: this will go away with the planned change of hosting.
- Pages: there is no limit on the number of pages. The
limits.truncatedfield flags a partial analysis: read it before drawing conclusions. - Content: the file must start with
%PDF-, otherwise the API replies415 NOT_A_PDF. - Metering: an analysis is counted as in the web interface (see Quotas). Document errors (
422), analysis service outages (502) and a PDF report that is too large (413 REPORT_TOO_LARGE) are not counted.
Code examples
The Node.js examples on this page require Node.js 18 or later (fetch is built in) and are written as ES modules: save them in a .mjs file, or add "type": "module" to your package.json.
Example: raw PDF
The body is the file itself. With curl, use --data-binary (not -d, which alters the bytes).
curl -X POST "https://veritas-doc.techstride.app/api/v1/analyze" \
-H "Authorization: Bearer vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/pdf" \
--data-binary @document.pdfimport requests
API_KEY = "vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
ENDPOINT = "https://veritas-doc.techstride.app/api/v1/analyze"
with open("document.pdf", "rb") as pdf:
response = requests.post(
ENDPOINT,
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/pdf",
},
data=pdf,
timeout=120,
)
if not response.ok:
raise SystemExit(f"{response.status_code} {response.text[:200]}")
body = response.json()
print(body["status"], body["confidence_score"])
print(body["disclaimer"])import { readFile } from "node:fs/promises";
const API_KEY = "vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const ENDPOINT = "https://veritas-doc.techstride.app/api/v1/analyze";
const pdf = await readFile("document.pdf");
const response = await fetch(ENDPOINT, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/pdf",
},
body: pdf,
});
if (!response.ok) {
console.error(response.status, (await response.text()).slice(0, 200));
process.exit(1);
}
const body = await response.json();
console.log(body.status, body.confidence_score);
console.log(body.disclaimer);Example: multipart form
Do not set the Content-Type header yourself: the client adds the form boundary to it.
curl -X POST "https://veritas-doc.techstride.app/api/v1/analyze" \
-H "Authorization: Bearer vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-F "file=@document.pdf;type=application/pdf"import requests
API_KEY = "vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
ENDPOINT = "https://veritas-doc.techstride.app/api/v1/analyze"
with open("document.pdf", "rb") as pdf:
response = requests.post(
ENDPOINT,
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": ("document.pdf", pdf, "application/pdf")},
timeout=120,
)
if not response.ok:
raise SystemExit(f"{response.status_code} {response.text[:200]}")
body = response.json()
print(body["status"], body["confidence_score"])
print(body["disclaimer"])import { readFile } from "node:fs/promises";
const API_KEY = "vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const ENDPOINT = "https://veritas-doc.techstride.app/api/v1/analyze";
const pdf = await readFile("document.pdf");
const form = new FormData();
form.append("file", new Blob([pdf], { type: "application/pdf" }), "document.pdf");
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}` },
body: form,
});
if (!response.ok) {
console.error(response.status, (await response.text()).slice(0, 200));
process.exit(1);
}
const body = await response.json();
console.log(body.status, body.confidence_score);
console.log(body.disclaimer);Response
A 200 response contains the full report. The table describes the main fields. The response may contain other fields: ignore the ones you do not know (see Versioning).
Main fields
| Field | Type | Description |
|---|---|---|
analysis_id | uuid | Identifier of the analysis. |
usage.source | string | What was counted: cabinet (subscription), free (free analysis), credit (credit), or repeat (same file sent again within the 10-minute retry window: nothing is counted). |
schema_version | string | Version of the report format, for example 2.0. |
status | string | Summary reading: CONFORME (no serious sign: a score of at least 80 and no finding of high severity), SUSPECT (a score from 50 to 79, or a finding of high severity) or ALTERE (a score below 50). A CONFORME report can still list minor findings: read findings for the detail. It is not a verdict. |
confidence_score | integer | Confidence score from 0 to 100. Each finding takes its weight off the score: the lower the score, the more numerous or marked the signs. |
risk_level | string | Level of attention: low, medium or high. |
document_info | object | File metadata: author, creator, producer, title, creation_date, modification_date, pdf_version, page_count, pages_analyzed, encrypted, has_xmp. A value missing from the file is null. |
editor_assessment | object | Detected editing tool: detected_tool, category (desktop_pro, office, online_editor, image_editor, scanner, generator or unknown), trust_score (0 to 100), creator_producer_mismatch (boolean) and rationale. |
findings[] | array | Findings. Each one has a stable code, a severity (info, low, medium or high), a weight (points taken off the score), a title, a detail and the page concerned, or null. |
pages[] | array | One entry per analysed page: index (from 0), width, height, zones[] and, on request only, preview. |
pages[].zones[] | array | Zones found on the page: id, type (ink, white_rect, redaction, image_overlay or annotation), severity, rect and, if text could be recovered under the zone, hidden_text and hidden_text_confidence (0 to 1). |
pages[].zones[].rect | object | Position of the zone: x, y, w, h, all normalised between 0 and 1 relative to the page dimensions. |
limits | object | max_pages (most pages analysed, null when there is no limit) and truncated (true if the analysis is partial: read it before drawing conclusions). |
disclaimer | string | Reminder of the scope of the report. To be passed on to end users. |
Example response
Abridged response for a two-page document. The title, detail and rationale texts are meant to be read by people and are currently written in French: base your logic on code, severity, category and status.
{
"analysis_id": "3f6b1c1e-8d0a-4b52-9a57-2d1e5f0c7a44",
"usage": { "source": "cabinet" },
"schema_version": "2.0",
"status": "SUSPECT",
"confidence_score": 55,
"risk_level": "medium",
"document_info": {
"author": null,
"creator": "Microsoft Word",
"producer": "Adobe Acrobat Pro",
"title": null,
"creation_date": "2026-03-02T09:14:00+01:00",
"modification_date": "2026-03-04T17:41:00+01:00",
"pdf_version": "1.7",
"page_count": 2,
"pages_analyzed": 2,
"encrypted": false,
"has_xmp": true
},
"editor_assessment": {
"detected_tool": "Adobe Acrobat",
"category": "desktop_pro",
"trust_score": 25,
"creator_producer_mismatch": true,
"rationale": "Créé avec Microsoft Word, produit par Adobe Acrobat. Modificateurs appliqués : mismatch Creator/Producer (-20), ModDate postérieure à CreationDate (-10)."
},
"findings": [
{
"code": "CREATOR_PRODUCER_MISMATCH",
"severity": "high",
"weight": 20,
"title": "L'outil de création et l'outil de production diffèrent",
"detail": "Créé avec Microsoft Word, produit par Adobe Acrobat. Modificateurs appliqués : mismatch Creator/Producer (-20), ModDate postérieure à CreationDate (-10).",
"page": null
},
{
"code": "INCREMENTAL_SAVE",
"severity": "medium",
"weight": 15,
"title": "Le document a été enregistré 2 fois",
"detail": "2 marqueurs startxref détectés. Un enregistrement incrémental conserve les versions précédentes dans le fichier.",
"page": null
},
{
"code": "MODDATE_AFTER_CREATION",
"severity": "medium",
"weight": 10,
"title": "Le document a été modifié après sa création",
"detail": "Création : 2026-03-02T09:14:00+01:00 — modification : 2026-03-04T17:41:00+01:00.",
"page": null
}
],
"pages": [
{ "index": 0, "width": 595.28, "height": 841.89, "zones": [] },
{ "index": 1, "width": 595.28, "height": 841.89, "zones": [] }
],
"limits": { "max_pages": null, "truncated": false },
"disclaimer": "Indices techniques. Ne constitue pas une preuve de fraude. Un document peut présenter ces indices pour des raisons parfaitement légitimes (ré-enregistrement, signature, export)."
}Example zone
When a rectangle covers text, the zone can carry the text recovered underneath. This example is illustrative.
{
"id": "p0z1",
"type": "white_rect",
"rect": { "x": 0.12, "y": 0.33, "w": 0.2, "h": 0.05 },
"severity": "high",
"hidden_text": "Solde : 12 480,00 €",
"hidden_text_confidence": 0.82
}Response headers
Successful responses carry these headers. Keep X-Request-Id: it identifies your request if you contact support.
| Header | Description |
|---|---|
X-Veritas-API-Version | API version, here 1. |
X-Request-Id | Identifier of the request. |
X-RateLimit-Limit | Number of requests allowed per minute for this key. |
X-RateLimit-Remaining | Requests left in the current window. |
Cache-Control | Always no-store: do not cache these responses. |
Annotated PDF report
JSON is the default format. To file the result with the case or hand it to a person, ask for the annotated PDF report: the pages of the original document with the flagged areas outlined, preceded by a summary page. It is the same report of technical signs of modification, laid out for reading.
What the report contains
- A summary page: status, confidence score, the signs found (name, severity, weight), the document sheet, the SHA-256 fingerprint of the analysed file and the disclaimer.
- The analysed pages of the original document, each flagged area outlined and numbered. The colour of the outline depends on the severity.
- After each annotated page, a legend page: type of area, position and, when there is one, the text found under the area.
The original content is not modified: the marks are drawn on top, in a layer that your PDF viewer can hide. Only the analysed pages are in the report; if the analysis is partial, the summary says so.
Requesting the report
Add ?format=pdf to the URL, or send Accept: application/pdf. The response body is the file: save it as is (with curl, -o releve.pdf). The request body is the same as for JSON.
curl -X POST "https://veritas-doc.techstride.app/api/v1/analyze?format=pdf" \
-H "Authorization: Bearer vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/pdf" \
--data-binary @document.pdf \
-o releve.pdf -w "HTTP %{http_code}\n"import requests
API_KEY = "vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
ENDPOINT = "https://veritas-doc.techstride.app/api/v1/analyze"
with open("document.pdf", "rb") as pdf:
response = requests.post(
ENDPOINT,
params={"format": "pdf"},
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/pdf",
},
data=pdf,
timeout=120,
)
if not response.ok:
# Errors are always JSON, even with format=pdf.
raise SystemExit(f"{response.status_code} {response.text[:200]}")
with open("releve.pdf", "wb") as report:
report.write(response.content)
print(response.headers["X-Veritas-Status"], response.headers.get("X-Veritas-Score"))import { readFile, writeFile } from "node:fs/promises";
const API_KEY = "vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const ENDPOINT = "https://veritas-doc.techstride.app/api/v1/analyze?format=pdf";
const pdf = await readFile("document.pdf");
const response = await fetch(ENDPOINT, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/pdf",
},
body: pdf,
});
if (!response.ok) {
// Errors are always JSON, even with format=pdf.
console.error(response.status, (await response.text()).slice(0, 200));
process.exit(1);
}
await writeFile("releve.pdf", Buffer.from(await response.arrayBuffer()));
console.log(response.headers.get("X-Veritas-Status"), response.headers.get("X-Veritas-Score"));Response
A 200 response has Content-Type application/pdf and the report as its body, offered as an attachment named releve-veritas- followed by the first 8 characters of the file fingerprint. There is no JSON body: the essentials of the outcome are repeated in the headers below, in addition to those listed under Response.
Report headers
| Header | Description |
|---|---|
Content-Disposition | Attachment named releve-veritas-<8 characters>.pdf. |
X-Veritas-Status | Equivalent of status: CONFORME, SUSPECT or ALTERE. |
X-Veritas-Score | Equivalent of confidence_score, from 0 to 100. |
X-Veritas-Analysis-Id | Equivalent of analysis_id. |
X-Veritas-Usage-Source | Equivalent of usage.source: cabinet, free, credit or repeat. |
X-Veritas-Truncated | Equivalent of limits.truncated: true if the analysis is partial. |
Example headers
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="releve-veritas-3f6b1c1e.pdf"
X-Veritas-API-Version: 1
X-Veritas-Status: SUSPECT
X-Veritas-Score: 55
X-Veritas-Analysis-Id: 3f6b1c1e-8d0a-4b52-9a57-2d1e5f0c7a44
X-Veritas-Usage-Source: cabinet
X-Veritas-Truncated: false
Cache-Control: no-storeErrors
Errors stay JSON, in the format described under Errors: check the HTTP status before treating the body as a PDF. A report larger than the maximum response size gives 413 REPORT_TOO_LARGE: nothing is counted, and you can ask for the same file again with format=json.
Example 413 response for a report that is too large:
{
"error": {
"code": "REPORT_TOO_LARGE",
"message": "Le relevé PDF annoté dépasse la taille maximale de réponse. Utilisez format=json ou réduisez le document."
}
}Size
The report weighs roughly as much as the analysed pages of the original document, plus a few tens of KB. Beyond 4 MB, the API replies 413 REPORT_TOO_LARGE. This limit comes from the response size accepted by the current platform: it will go away with the planned change of hosting.
Metering
As for JSON: one analysis is counted per successful call, and none on failure, REPORT_TOO_LARGE included (a report that is not delivered is not billed). Sending the same file again within 10 minutes, in the other format, is a retry: X-Veritas-Usage-Source: repeat, nothing more is counted.
JSON or PDF?
| Criterion | JSON (default) | Annotated PDF report |
|---|---|---|
| Use | Software integration, automated processing, dashboards. | Filing with the case, handing to a person, human reading. |
| Content | Every field, area coordinates, previews on request. | Summary, outlined original pages, text found, file fingerprint. |
| Weight | Light: previews are removed by default. | Close to the weight of the analysed pages; refused beyond 4 MB. |
| Metering | One analysis per successful call. | Identical. |
Balance and usage
GET/api/v1/usage
GET /api/v1/usage returns the state of the account the key belongs to. The call uses up no analysis: use it to check a key or to anticipate a quota. It does count towards the rate limit (see Quotas).
Example call
curl "https://veritas-doc.techstride.app/api/v1/usage" \
-H "Authorization: Bearer vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"import requests
API_KEY = "vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
response = requests.get(
"https://veritas-doc.techstride.app/api/v1/usage",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
response.raise_for_status()
usage = response.json()
print(usage["plan"], usage["can_analyze"])const API_KEY = "vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const response = await fetch("https://veritas-doc.techstride.app/api/v1/usage", {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const usage = await response.json();
console.log(usage.plan, usage.can_analyze);Example response
{
"plan": "cabinet",
"credits": 0,
"free_remaining": 0,
"cabinet": {
"used": 12,
"limit": 400,
"period_end": "2026-10-19T00:00:00.000Z"
},
"can_analyze": true
}Fields
| Field | Type | Description |
|---|---|---|
plan | string | Plan of the account, for example cabinet. |
credits | integer | Analysis credits left. |
free_remaining | integer | Free analyses left. |
cabinet | object | null | null without an active Firm subscription. Otherwise: used (documents analysed in the period), limit (quota for the period) and period_end (end of the period, ISO 8601). |
can_analyze | boolean | true if an analysis can be started now. |
Errors
An error returns an HTTP status and a JSON body of this shape. Base your logic on the status and on code: the message is human-readable text and may change. With format=pdf, errors are still JSON: check the HTTP status before treating the body as a PDF.
{
"error": {
"code": "QUOTA_EXHAUSTED",
"message": "Aucune analyse disponible sur ce compte."
}
}| HTTP | Code | Case | Retry? |
|---|---|---|---|
| 400 | EMPTY_FILEBAD_REQUEST | Empty body, Content-Type other than application/pdf or multipart/form-data, missing file field, invalid form or invalid format parameter (accepted values: json, pdf). | No |
| 401 | MISSING_API_KEYINVALID_API_KEY | Key missing, malformed, unknown, revoked or expired. | No |
| 402 | QUOTA_EXHAUSTED | No analysis available on this account. | No |
| 403 | API_ACCESS_NOT_ALLOWED | The account has no API access. | No |
| 413 | FILE_TOO_LARGE | File larger than the size limit configured on this service (there is no limit by default). | No |
| 413 | REPORT_TOO_LARGE | format=pdf: the annotated report exceeds the maximum response size. Not counted. Ask for format=json or reduce the document. | No, not as is |
| 415 | NOT_A_PDF | The content does not start with %PDF-. | No |
| 422 | PDF_ENCRYPTEDPDF_CORRUPTEDNO_PAGES | The document cannot be analysed (encrypted, corrupted or without pages). Not counted. | No |
| 429 | RATE_LIMITED | Too many requests for this key. The Retry-After header gives the wait, in seconds. | Yes, after Retry-After |
| 502 | ENGINE_ERROR | Analysis service outage, without detail. Not counted. | Yes, with exponential backoff |
| 503 | SERVICE_UNAVAILABLE | Service temporarily unavailable (database or quotas): no analysis is started. | Yes, with exponential backoff |
Quotas and rate limit
Analysis quota
An analysis started through the API is counted like one started from the interface: first the monthly quota of the Firm subscription, then free analyses, then credits. The usage.source field of the response tells you what was used.
Sending the same file again from the same account within 10 minutes is a retry, not a new analysis: the response carries usage.source: "repeat" and nothing is counted. Retrying within 10 minutes of a network drop therefore uses no new analysis; a retry made later is counted as a new analysis. Every call, retries included, does count towards the rate limit. Call GET /api/v1/usage to check your balance.
Rate limit
Each key can send 60 requests per minute, over a fixed one-minute window. Beyond that, the API replies 429 RATE_LIMITED with a Retry-After header, in seconds. The X-RateLimit-Remaining header of successful responses lets you slow down before you reach the limit.
Recovering from errors
Only retry transient errors, with a delay that grows.
- On
429, wait at leastRetry-Afterseconds before retrying. - On
502and503, wait 1 s, then 2 s, 4 s, 8 s… capping the delay (60 s for example) and adding a little randomness, so that several clients do not all restart at the same moment. - Limit the number of attempts (5 for example) and log
X-Request-Id. - Do not retry other
4xxerrors: the same request would give the same result.
Retry example
import random
import time
import requests
API_KEY = "vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
ENDPOINT = "https://veritas-doc.techstride.app/api/v1/analyze"
RETRYABLE = {429, 502, 503}
def analyze(path, max_attempts=5):
for attempt in range(max_attempts):
with open(path, "rb") as pdf:
response = requests.post(
ENDPOINT,
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/pdf",
},
data=pdf,
timeout=120,
)
if response.status_code not in RETRYABLE:
break
if attempt == max_attempts - 1:
break
retry_after = response.headers.get("Retry-After", "")
if retry_after.isdigit():
delay = int(retry_after)
else:
delay = min(2 ** attempt, 60) + random.random()
time.sleep(delay)
return response
response = analyze("document.pdf")
print(response.status_code)import { readFile } from "node:fs/promises";
const API_KEY = "vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const ENDPOINT = "https://veritas-doc.techstride.app/api/v1/analyze";
const RETRYABLE = new Set([429, 502, 503]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function analyze(path, maxAttempts = 5) {
const pdf = await readFile(path);
let response;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
response = await fetch(ENDPOINT, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/pdf",
},
body: pdf,
});
if (!RETRYABLE.has(response.status)) break;
if (attempt === maxAttempts - 1) break;
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs =
retryAfter > 0
? retryAfter * 1000
: Math.min(2 ** attempt * 1000, 60000) + Math.random() * 1000;
await sleep(delayMs);
}
return response;
}
const response = await analyze("document.pdf");
console.log(response.status);Versioning and changes
- The version is in the path:
/api/v1. Every response carries theX-Veritas-API-Version: 1header. - Within a version, changes are additive only: new fields, new endpoints. Your code must ignore fields it does not know and tolerate an unexpected value in an enumerated field (
code,category,type). - Any breaking change, such as a field removed or renamed, creates a new version:
/api/v2. v1 is never changed in a breaking way. - This page is the reference documentation. No OpenAPI file is published.
Security best practices
An access key grants the right to analyse on behalf of your account, and therefore to use up your quota.
Keep the key on the server
Call the API from your backend only. Never put the key in a web page, a mobile app, a code repository or a versioned configuration file. Calls from a browser are not supported anyway (no CORS header).
Read it from the environment
Store the key in an environment variable or a secrets manager (
process.env.VERITAS_API_KEY,os.environ["VERITAS_API_KEY"]) and write it neither in logs nor in tickets.One key per use
An account can have 5 active keys. Create one key per application or environment (production, staging) and name it clearly: you can then revoke one without interrupting the others.
Rotate your keys
Replace your keys at regular intervals: create the new key, deploy it, check that it works, then revoke the old one.
Revoke without delay when in doubt
From My account, revocation is immediate: the key stops working at once. If a key may have leaked, revoke it first, then create a new one.
Treat PDFs and reports as sensitive data
The documents you send may contain personal data. Keep PDFs and reports only as long as necessary, and never send a key or a document by email.