Skip to content
thisverify
Developers

ThisVerify API

A simple REST API: send a file, get a full JSON report.

Overview

The ThisVerify API takes a document (PDF or image) and returns a full forgery report: a 0–100 risk score, a verdict (low risk / review / high risk), findings with exact locations on the document, arithmetic checks and extracted data. It is a JSON REST API and works asynchronously: create a scan, then receive the result by webhook or by polling.

Base URL
https://thisverify-green.vercel.app/api/v1
Version
2026-10-01
Format
JSON · UTF-8 · HTTPS
  1. 1. Submit
    POST /scans with the file. Immediate 202 with a scan id.
  2. 2. Analysis
    Engines run in parallel — usually 20–60 seconds.
  3. 3. Result
    scan.completed webhook, or GET /scans/{id}.

Authentication

Every request carries an API key in the Authorization header. Keys belong to the organization (not to one user), are created under "API & integrations" in the dashboard, are shown once, and are stored only as a hash. Keep one key per system and revoke each separately.

API access — on paid plans

API keys and webhooks are available to organizations on the Business or Enterprise plan. Docs, examples and the spec are open to everyone, so you can plan the integration now. A key from an organization without a suitable plan gets 403 NO_API_ACCESS.

Plans & pricing
Authorization: Bearer tv_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Never ship a key in client-side code (browser/app). Call the API from your server only.

Scopes

Each key has scopes. Give every system only what it needs.

FieldTypeDescription
scans:writescopeCreate scans
scans:readscopeRead scans and reports
scans:deletescopeDelete scans
usage:readscopeRead usage and quota

Quick start

Your first check in two calls:

curl -X POST https://thisverify-green.vercel.app/api/v1/scans \
  -H "Authorization: Bearer $THISVERIFY_KEY" \
  -H "Idempotency-Key: loan-88231-doc-1" \
  -F "file=@transfer.pdf" \
  -F 'context={"document_type":"bank_transfer","expected_amount":"12500","expected_name":"ישראל ישראלי"}' \
  -F "client_reference=loan-88231"

# → 202 {"id":"8f1c…","object":"scan","status":"queued",…}

curl https://thisverify-green.vercel.app/api/v1/scans/8f1c… -H "Authorization: Bearer $THISVERIFY_KEY"

Create a scan

POST/api/v1/scans

Three ways to send the file: multipart (recommended), JSON with base64, or JSON with a public https URL we download from. Returns 202 with the scan in status queued. Requires scans:write. Each scan uses one unit of the organization’s monthly allowance; if analysis fails on our side the unit is refunded.

FieldTypeDescription
filebinaryThe file (multipart). PDF, JPEG, PNG, WEBP or HEIC.
file.content_base64stringJSON alternative: file content as base64 (data URLs accepted), with file.name.
file_urlstringJSON alternative: a public https URL. Must return the file within 15 s, without redirects. Internal addresses are blocked.
context.document_typeenumDocument type hint: bank_transfer, payment_app, bank_statement, account_confirmation, balance_confirmation, check, deposit_receipt, card_statement, payslip, invoice, tax_form, id_document, utility_bill, loan_document, medical, contract, other
context.expected_amountstringThe amount you expect. A mismatch becomes a finding.
context.expected_namestringExpected payee / account holder / employee name.
context.expected_dateYYYY-MM-DDExpected date.
context.expected_referencestringExpected reference / account number.
context.notesstringFree-text context for the analyst (up to 1,000 chars).
client_referencestringYour own id (application/case number). Returned everywhere and filterable.
metadataobjectUp to 20 key-value pairs (values up to 500 chars). In multipart, send as a JSON string.
lang"he" | "en"Report language. Default: he.
JSON
POST /api/v1/scans
Content-Type: application/json
Idempotency-Key: 4c1f…

{
  "file": { "name": "payslip-06.pdf", "content_base64": "JVBERi0xLjcK…" },
  "context": { "document_type": "payslip", "expected_name": "Dana Levi" },
  "client_reference": "app-55102",
  "metadata": { "branch": "tel-aviv", "officer": "u-1182" },
  "lang": "en"
}

Retrieve a scan

GET/api/v1/scans/{id}

Returns the scan. When status is completed it includes the full report. Add ?include=summary for the summary only (no report). Requires scans:read. While processing, stage shows the current step.

200 Response
{
  "id": "8f1c2a4e-…",
  "object": "scan",
  "status": "completed",
  "verdict": "high_risk",
  "risk_score": 91,
  "document_type": "bank_transfer",
  "issuer": "בנק הפועלים",
  "channel": "app_screenshot",
  "client_reference": "loan-88231",
  "report": {
    "summary": "…",
    "recommended_actions": ["…"],
    "findings": [
      {
        "id": "ai-1",
        "category": "visual",
        "severity": "high",
        "confidence": 0.93,
        "title": "Amount digits re-rendered",
        "detail": "The digits of the amount use a different font weight and anti-aliasing…",
        "box": { "page": 1, "box_2d": [412, 520, 446, 700] }
      }
    ],
    "checks": [ { "id": "arith", "label": "…", "status": "fail", "detail": "…" } ],
    "extracted": { "amounts": ["12,500.00"], "dates": ["2026-10-05"], "iban": "IL62…" },
    "forensic_analysis": { "pixel_variance": { "has_variance": true, "score": 0.88 } },
    "engine": { "version": "…", "duration_ms": 31244, "layers": ["pdf", "metadata", "ela", "content", "ai"] }
  }
}

box_2d is [ymin, xmin, ymax, xmax] on a 0–1000 scale relative to the page, so you can draw it at any resolution.

List scans

GET/api/v1/scans

Newest first, cursor-paginated. Requires scans:read.

FieldTypeDescription
limit1–100Page size. Default 25.
cursorstringnext_cursor from the previous page.
statusenumqueued · processing · completed · failed
verdictenumlow_risk · review · high_risk
client_referencestringFilter by your id.
created_after / created_beforeISO 8601Date range.
{
  "object": "list",
  "data": [ { "id": "…", "object": "scan", "status": "completed", "verdict": "review", … } ],
  "has_more": true,
  "next_cursor": "eyJ0IjoiMjAyNi0xMC0w…"
}

Delete a scan

DELETE/api/v1/scans/{id}

Permanently deletes the scan, its report and the original file (including any training copies). Requires scans:delete. Use it for privacy / GDPR erasure requests.

Usage

GET/api/v1/usage

The organization’s usage in the current period. Requires usage:read.

{
  "object": "usage",
  "plan": { "id": "business", "name": "Business" },
  "period": { "start": "2026-10-01T00:00:00.000Z", "end": "2026-11-01T00:00:00.000Z" },
  "used": 1840,
  "limit": 5000,
  "remaining": 3160,
  "total_scans": 22114
}

To check a key: GET /api/v1 returns the key’s organization and scopes.

Webhooks

Instead of polling, register an https endpoint under "API & integrations" and we POST when a scan finishes. Each endpoint has its own signing secret (whsec_…).

FieldTypeDescription
scan.completedeventThe scan finished. Includes verdict, risk_score, client_reference and metadata. Fetch GET /scans/{id} for the full report.
scan.failedeventThe scan failed (e.g. corrupt file). The unit was refunded.
pingeventTest event sent from the "Send test" button.
POST → your endpoint
ThisVerify-Event: scan.completed
ThisVerify-Delivery: 5b0e…            ← unique event id (dedupe on it)
ThisVerify-Signature: t=1791364364,v1=3f9a…

{
  "id": "5b0e…",
  "type": "scan.completed",
  "created": "2026-10-07T09:13:21.004Z",
  "data": {
    "scan": {
      "id": "8f1c…", "object": "scan", "status": "completed",
      "verdict": "high_risk", "risk_score": 91, "document_type": "bank_transfer",
      "client_reference": "loan-88231", "metadata": { "branch": "tel-aviv" },
      "file": { "name": "transfer.pdf", "sha256": "…" }
    }
  }
}
  • • Return 2xx within 10 seconds. Do heavy work asynchronously after responding.
  • • Retries: immediately, after 3 s and 10 s, then after 1 min, 5 min, 30 min, 2 h and 12 h.
  • • Duplicates are possible — dedupe on ThisVerify-Delivery.
  • • After 50 consecutive failures the endpoint is disabled; re-enable it from the dashboard.

Verifying signatures

Make sure every request came from us: compute HMAC-SHA256 over "{t}.{raw body}" with the webhook secret, compare to v1 in constant time, and reject requests whose t is older than 5 minutes.

import crypto from 'node:crypto';

export function verifyThisVerify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected), b = Buffer.from(parts.v1 || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: app.post('/hooks/thisverify', express.raw({ type: 'application/json' }), (req, res) => {
//   if (!verifyThisVerify(req.body.toString(), req.get('ThisVerify-Signature'), process.env.TV_WHSEC)) return res.sendStatus(400);
//   res.sendStatus(200); queue.push(JSON.parse(req.body));
// });

Idempotency

Send an Idempotency-Key header (up to 200 chars, e.g. your document id) on every POST /scans. Retrying with the same key and file returns the same scan (200 with Idempotent-Replayed: true) without another charge. The same key with a different file returns 409. Keys are kept for 7 days.

Rate limits

Limits are per organization per minute (default 120; enterprise plans by agreement). Every response includes RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. When exceeded you get 429 with Retry-After in seconds. Maximum file size: 10 MB.

Errors

Every error has the same shape with a stable code and a request_id (send it to support).

HTTP/1.1 402 Payment Required
X-Request-Id: req_0c51d0a3e8b14f2a9d11

{ "error": { "code": "QUOTA_EXCEEDED", "message": "The organization has used its monthly allowance.", "request_id": "req_0c51d0a3e8b14f2a9d11" } }
HTTPcodeMeaning
401NO_KEYMissing API key. Send "Authorization: Bearer tv_live_…".
401INVALIDThe API key is invalid or revoked.
403SUSPENDEDThe organization or key owner is suspended.
403NO_API_ACCESSAPI access requires a paid plan that includes the API (Business or Enterprise). Upgrade from the dashboard or contact sales.
403INSUFFICIENT_SCOPEThe API key does not have the required scope.
429RATE_LIMITEDToo many requests. Retry after the time in the Retry-After header.
400INVALID_REQUESTThe request is malformed.
400FILE_REQUIREDProvide a file as multipart "file", JSON "file.content_base64", or JSON "file_url".
400EMPTY_FILEThe file is empty.
413FILE_TOO_LARGEThe file exceeds the size limit.
415UNSUPPORTED_FILEUnsupported file type. Use PDF, JPEG, PNG, WEBP or HEIC.
400FILE_URL_REJECTEDfile_url must be a public https URL that returns the file within 15 seconds.
402QUOTA_EXCEEDEDThe organization has used its monthly allowance.
503AI_NOT_CONFIGUREDThe analysis service is temporarily unavailable.
404NOT_FOUNDResource not found.
409CONFLICTAn Idempotency-Key was reused with a different request.
500SERVICE_ERRORUnexpected error. Retry with the same Idempotency-Key.

The scan object

FieldTypeDescription
statusenumqueued → processing → completed / failed
verdictenumlow_risk — no significant signs · review — needs a human look · high_risk — clear signs of forgery
risk_score0–100Weighted score across all analysis layers.
document_type / issuer / channelstringDocument type, issuer (bank/employer/HMO) and capture channel (native PDF, screenshot, photo…).
report.findings[]arrayFindings: category, severity (info/low/medium/high/critical), confidence (0–1), title, detail and box.
report.checks[]arrayDeterministic checks: arithmetic, balances, VAT, IBAN, ID check digit, bank code, weekday and more — pass/warn/fail.
report.extractedobjectExtracted data: amounts, dates, names, accounts, transactions.
labelenum | nullYour human label (genuine/fraud), if set in the dashboard.

OpenAPI

The full OpenAPI 3.1 specification — import it into Postman or Insomnia, or generate an SDK in any language.