Skip to main content

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: Bearer header
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: 1 header

Quick start

Three steps are enough to get a first report.

  1. 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).

  2. 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.

    shell
    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.pdf
  3. Read the response

    The response is the report, as JSON. Read status, confidence_score and findings first, then pass disclaimer on 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.

http
Authorization: Bearer vd_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

A 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

Authentication errors
CodeHTTPCase
MISSING_API_KEY401Header missing, or malformed key.
INVALID_API_KEY401Unknown, revoked or expired key. The message is identical in all three cases, so that nothing is revealed.
API_ACCESS_NOT_ALLOWED403The 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

Request body
FormatContent-TypeBody
Raw PDFapplication/pdfThe bytes of the file, as they are.
Formmultipart/form-dataA 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.truncated field flags a partial analysis: read it before drawing conclusions.
  • Content: the file must start with %PDF-, otherwise the API replies 415 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.pdf

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"

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

Main fields
FieldTypeDescription
analysis_iduuidIdentifier of the analysis.
usage.sourcestringWhat 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_versionstringVersion of the report format, for example 2.0.
statusstringSummary 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_scoreintegerConfidence 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_levelstringLevel of attention: low, medium or high.
document_infoobjectFile 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_assessmentobjectDetected 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[]arrayFindings. 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[]arrayOne entry per analysed page: index (from 0), width, height, zones[] and, on request only, preview.
pages[].zones[]arrayZones 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[].rectobjectPosition of the zone: x, y, w, h, all normalised between 0 and 1 relative to the page dimensions.
limitsobjectmax_pages (most pages analysed, null when there is no limit) and truncated (true if the analysis is partial: read it before drawing conclusions).
disclaimerstringReminder 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.

json
{
  "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.

json
{
  "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.

Response headers
HeaderDescription
X-Veritas-API-VersionAPI version, here 1.
X-Request-IdIdentifier of the request.
X-RateLimit-LimitNumber of requests allowed per minute for this key.
X-RateLimit-RemainingRequests left in the current window.
Cache-ControlAlways 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"

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

Report headers
HeaderDescription
Content-DispositionAttachment named releve-veritas-<8 characters>.pdf.
X-Veritas-StatusEquivalent of status: CONFORME, SUSPECT or ALTERE.
X-Veritas-ScoreEquivalent of confidence_score, from 0 to 100.
X-Veritas-Analysis-IdEquivalent of analysis_id.
X-Veritas-Usage-SourceEquivalent of usage.source: cabinet, free, credit or repeat.
X-Veritas-TruncatedEquivalent of limits.truncated: true if the analysis is partial.

Example headers

http
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-store

Errors

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:

json
{
  "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?

Comparison of the JSON and PDF formats
CriterionJSON (default)Annotated PDF report
UseSoftware integration, automated processing, dashboards.Filing with the case, handing to a person, human reading.
ContentEvery field, area coordinates, previews on request.Summary, outlined original pages, text found, file fingerprint.
WeightLight: previews are removed by default.Close to the weight of the analysed pages; refused beyond 4 MB.
MeteringOne 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"

Example response

json
{
  "plan": "cabinet",
  "credits": 0,
  "free_remaining": 0,
  "cabinet": {
    "used": 12,
    "limit": 400,
    "period_end": "2026-10-19T00:00:00.000Z"
  },
  "can_analyze": true
}

Fields

Fields
FieldTypeDescription
planstringPlan of the account, for example cabinet.
creditsintegerAnalysis credits left.
free_remainingintegerFree analyses left.
cabinetobject | nullnull 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_analyzebooleantrue 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.

json
{
  "error": {
    "code": "QUOTA_EXHAUSTED",
    "message": "Aucune analyse disponible sur ce compte."
  }
}
Errors
HTTPCodeCaseRetry?
400EMPTY_FILEBAD_REQUESTEmpty 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
401MISSING_API_KEYINVALID_API_KEYKey missing, malformed, unknown, revoked or expired.No
402QUOTA_EXHAUSTEDNo analysis available on this account.No
403API_ACCESS_NOT_ALLOWEDThe account has no API access.No
413FILE_TOO_LARGEFile larger than the size limit configured on this service (there is no limit by default).No
413REPORT_TOO_LARGEformat=pdf: the annotated report exceeds the maximum response size. Not counted. Ask for format=json or reduce the document.No, not as is
415NOT_A_PDFThe content does not start with %PDF-.No
422PDF_ENCRYPTEDPDF_CORRUPTEDNO_PAGESThe document cannot be analysed (encrypted, corrupted or without pages). Not counted.No
429RATE_LIMITEDToo many requests for this key. The Retry-After header gives the wait, in seconds.Yes, after Retry-After
502ENGINE_ERRORAnalysis service outage, without detail. Not counted.Yes, with exponential backoff
503SERVICE_UNAVAILABLEService 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 least Retry-After seconds before retrying.
  • On 502 and 503, 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 4xx errors: 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)

Versioning and changes

  • The version is in the path: /api/v1. Every response carries the X-Veritas-API-Version: 1 header.
  • 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.