Veridact Compliance API

Token-authenticated subject screening for mid-market integrations

Run KYC, sanctions, PEP, and adverse-media checks against 20+ global data sources from your own product. Mints in one call, rotates without downtime, and writes every action to your audit trail — built for FinCEN 2026 and EU 6AMLD.

Get a token →
Bearer-token auth
60 checks / hour
Granular audit trail

Authentication model

Every API call is authenticated with a bearer token named vda_… — sent in the standard Authorization: Bearer vda_… header. Tokens are 168-bit random secrets scoped to a single Veridact account; they are not shared across teams, not embedded in client-side code, and never logged after issuance.

The plaintext secret is shown to you exactly once, at mint time. After that, only its SHA-256 hash is stored — if you lose the plaintext, you mint a new key. This design mirrors how modern signing keys (HMAC, OAuth client secrets) are handled: nothing on our side can reconstruct what you hold.

1. RequestAuthenticated account calls the mint endpoint inside /portal.
2. MintServer generates a 168-bit vda_… secret and hashes it.
3. StorePlaintext is returned once. Only the SHA-256 hash + prefix are persisted.
4. UseCaller sends Authorization: Bearer vda_… on every request.
5. RevokeCaller deletes the key in /portal; hash is flagged as revoked.

Rotation works without downtime: mint a new key, swap the bearer header in your client, then revoke the old one once your traffic rolls over. Revocation is immediate — a deleted key returns 401 invalid_key on the very next request. Read the security posture page for the broader conversation about API credential handling.

Endpoints exposed

The Compliance API exposes v1 endpoints for kicking off single or batch subject-check pipeline runs. All endpoints live under /api/v1/compliance/* and require bearer-token authentication. Registered callbacks receive the same signed lifecycle for every check created by either submission path.

Method Path Auth Description
POST /api/v1/compliance/subject-checks Bearer vda_… Start a Person Check pipeline run for the bearer token's account. Returns 201 with a subject_check_id to poll.
POST /api/v1/compliance/subject-checks/batch Bearer vda_… Start up to 50 checks in one request. Each accepted subject receives its own lifecycle callbacks and remains independently tenant-scoped.
POST /api/v1/compliance/subject-checks/sandbox Bearer vda_… Synchronous mock verdict for client-flow testing. Same response shape as subject-checks with a deterministic clear_no_match; no DB writes, no quota increment, no rate-limit decrement.

POST /api/v1/compliance/subject-checks

Starts a Person Check screening run for one subject. The endpoint validates the payload, inserts a person_checks row, increments the account quota, and asynchronously kicks off the matching/scoring pipeline. The HTTP response returns immediately with a run id; polling for the verdict happens against your own account data.

Request body
// POST /api/v1/compliance/subject-checks
{
  "first_name": "Jane",
  "middle_name": "Marie",           // optional
  "last_name": "Reyes",
  "dob": "1985-04-12",                   // optional, ISO 8601 date
  "aliases": ["J. Reyes"],            // optional array of strings
  "locations": [{                          // optional array
    "country": "US",
    "state": "NY",
    "city": "New York"
  }],
  "additional_identifiers": {                // optional free-form object
    "nationality": "US",
    "passport_country": "US"
  },
  "purpose": "customer onboarding",     // free text, recorded in audit
  "consent_given": true                  // required boolean; consent-obtained attestation
}
Successful response — 201
{
  "success": true,
  "subject_check_id": "ck_a7b3...",
  "status": "running",
  "subject_name": "Jane Marie Reyes",
  "verdict": null,
  "created_at": "2026-07-30T14:02:11.082Z"
}
Validation error — 400
{
  "error": {
    "code": "invalid_payload",
    "message": "First name is required",
    "errors": ["First name is required", "Last name is required"]
  }
}

Request & response examples

All requests go to https://veridact.solutions/api/v1/compliance authenticated with Authorization: Bearer vda_your_token_here. The three flavors below — curl, Python (requests), and JavaScript (fetch) — are functionally equivalent and call the same endpoints with the same payload.

POST /api/v1/compliance/subject-checks

Kick off a Person Check pipeline run. Returns HTTP 201 with a subject_check_id you poll against.

curl
curl -i -X POST https://veridact.solutions/api/v1/compliance/subject-checks \
  -H "Authorization: Bearer vda_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Jane",
    "middle_name": "Marie",
    "last_name": "Reyes",
    "dob": "1985-04-12",
    "aliases": ["J. Reyes"],
    "purpose": "customer onboarding",
    "consent_given": true
  }'
Python (requests)
import requests

resp = requests.post(
  "https://veridact.solutions/api/v1/compliance/subject-checks",
  headers={"Authorization": f"Bearer vda_your_token_here"},
  json={
    "first_name": "Jane",
    "middle_name": "Marie",
    "last_name": "Reyes",
    "dob": "1985-04-12",
    "aliases": ["J. Reyes"],
    "purpose": "customer onboarding",
    "consent_given": True,
  },
  timeout=30,
)

print(resp.status_code, resp.json())
JavaScript (fetch)
const resp = await fetch(
  "https://veridact.solutions/api/v1/compliance/subject-checks",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer vda_your_token_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      first_name: "Jane",
      middle_name: "Marie",
      last_name: "Reyes",
      dob: "1985-04-12",
      aliases: ["J. Reyes"],
      purpose: "customer onboarding",
      consent_given: true,
    }),
  }
);

const data = await resp.json();
console.log(resp.status, data);
Sample response — 201
{
  "success": true,
  "subject_check_id": "ck_a7b3f2",
  "status": "running",
  "subject_name": "Jane Marie Reyes",
  "verdict": null,
  "created_at": "2026-07-30T14:02:11.082Z"
}

GET /api/v1/compliance/subject-checks/:id

Poll the stored verdict for a previously-started run. verdict stays null while the pipeline is running and only resolves once status === "completed".

curl
curl -i -X GET https://veridact.solutions/api/v1/compliance/subject-checks/12345 \
  -H "Authorization: Bearer vda_your_token_here"
Python (requests)
import requests

subject_check_id = 12345

resp = requests.get(
  f"https://veridact.solutions/api/v1/compliance/subject-checks/{subject_check_id}",
  headers={"Authorization": f"Bearer vda_your_token_here"},
  timeout=30,
)

print(resp.status_code, resp.json())
JavaScript (fetch)
const subjectCheckId = 12345;

const resp = await fetch(
  `https://veridact.solutions/api/v1/compliance/subject-checks/${subjectCheckId}`,
  {
    method: "GET",
    headers: {
      "Authorization": "Bearer vda_your_token_here",
    },
  }
);

const data = await resp.json();
console.log(resp.status, data);
Sample response — 200
{
  "success": true,
  "subject_check_id": 12345,
  "status": "completed",
  "subject_name": "Jane Marie Reyes",
  "verdict": "clear",
  "created_at": "2026-07-30T14:02:11.082Z"
}

While the pipeline is still running you get the same shape but with "status": "running" and "verdict": null — poll until status is "completed" before reading verdict.

POST /api/v1/compliance/webhooks

Registers (or updates) an https:// callback URL for the calling token's account. Veridact POSTs the full signed lifecycle for each check created by POST /api/v1/compliance/subject-checks or POST /api/v1/compliance/subject-checks/batch: processing, review-required when applicable, escalation when accepted, and the completed or failed terminal event. Deliveries use a per-subscription HMAC secret returned in this response; event order is not guaranteed.

Request body
// POST /api/v1/compliance/webhooks
{
  "url": "https://hooks.example.com/veridact/results"
}
Successful response — 200
{
  "success": true,
  "subscription_id": "wh_sub_a1f3...",
  "url": "https://hooks.example.com/veridact/results",
  "signing_secret": "whsec_3x9p...",
  "created_at": "2026-07-30T14:02:11.082Z"
}
Lifecycle payload summary
EventStatusEvent timestampDecision context
subject_check.processingrunningprocessing_atCheck and owning account_id.
subject_check.review_requiredneeds_reviewreview_required_atInconclusive result, confidence, finding counts, source coverage, and review reason.
subject_check.escalatedescalatedescalated_atInvestigation, triggering tiers and scores, requested scope, and priority.
subject_check.completedcompletedcompleted_atExisting overall_result, confidence_level, and findings_count.
subject_check.failedfailedfailed_atExisting error_message.

Every lifecycle payload includes subject_check_id, the tenant account_id, and its stage status. See the full webhook reference and canonical JSON examples for field-level schemas, raw-body verification, idempotency, and the unchanged completed/failed compatibility contract.

Every delivery's body is HMAC-SHA256–signed with the per-subscription signing_secret returned above, and arrives on the X-Veridact-Signature header as sha256=<hex>. Verify the signature against the raw request bytes — before any JSON parsing — using the snippets below, then respond 200 on success or 400 on mismatch so the delivery is not retried.

Verifying the signature — Node.js (built-in crypto + http only)
// node-webhook-receiver.js — drop-in verifier.
// Buffers the raw body byte-for-byte so the HMAC matches Verdicts exactly.
const http = require('http');
const crypto = require('crypto');

const PORT = process.env.PORT || 8787;
const SIGNING_SECRET = process.env.VERIDACT_WEBHOOK_SECRET;

const server = http.createServer((req, res) => {
  const chunks = [];
  req.on('data', (c) => chunks.push(c));
  req.on('end', () => {
    const rawBody = Buffer.concat(chunks);

    const signature = (req.headers['x-veridact-signature'] || '').toString();
    const expected = 'sha256=' + crypto
      .createHmac('sha256', SIGNING_SECRET)
      .update(rawBody)
      .digest('hex');

    const sigBuf = Buffer.from(signature, 'utf8');
    const expBuf = Buffer.from(expected, 'utf8');
    const verified = sigBuf.length === expBuf.length
      && crypto.timingSafeEqual(sigBuf, expBuf);

    console.log(`signature_verified=${verified} signature=${signature} expected=${expected}`);

    if (verified) {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end('{"ok":true}');
    } else {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end('{"ok":false,"reason":"signature_mismatch"}');
    }
  });
});

server.listen(PORT, () => console.log(`listening on :${PORT}`));

# Example run — precomputed digest for the body below.
# export VERIDACT_WEBHOOK_SECRET='whsec_3x9pK7qZvY8R2mN4cQwL5jX6bDfHaEgT'
# node node-webhook-receiver.js &
# curl -sS -X POST http://localhost:8787/veridact \
#   -H 'Content-Type: application/json' \
#   -H 'X-Veridact-Signature: sha256=9519693faae7420007458e2b9213460caf5bbe1d7b6c9d3b8e6d31f77ffbc192' \
#   --data-binary '{"event":"subject_check.completed","subject_check_id":"ck_a7b3f2","status":"completed","verdict":"clear","created_at":"2026-07-30T14:02:11.082Z"}'
# expected: signature_verified=true signature=sha256=... expected=sha256=...  response {"ok":true}
Verifying the signature — Python (stdlib only)
# webhook_receiver.py — stdlib-only verifier.
# Reads raw body bytes off self.rfile so the HMAC matches Verdicts byte-for-byte.
import hashlib
import hmac
import os
from http.server import BaseHTTPRequestHandler, HTTPServer

SIGNING_SECRET = os.environ['VERIDACT_WEBHOOK_SECRET'].encode('utf-8')


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get('Content-Length', '0'))
        raw_body = self.rfile.read(length)

        signature = self.headers.get('X-Veridact-Signature', '')
        expected = 'sha256=' + hmac.new(SIGNING_SECRET, raw_body, hashlib.sha256).hexdigest()

        verified = hmac.compare_digest(signature, expected)
        print(f'signature_verified={verified} signature={signature} expected={expected}')

        if verified:
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(b'{"ok":true}')
        else:
            self.send_response(400)
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(b'{"ok":false,"reason":"signature_mismatch"}')

    def log_message(self, fmt, *args):
        pass  # quieter — we already print signature_verified=...


if __name__ == '__main__':
    HTTPServer(('127.0.0.1', 8787), Handler).serve_forever()

# Example run — precomputed digest for the body below.
# export VERIDACT_WEBHOOK_SECRET='whsec_3x9pK7qZvY8R2mN4cQwL5jX6bDfHaEgT'
# python3 webhook_receiver.py &
# curl -sS -X POST http://localhost:8787/veridact \
#   -H 'Content-Type: application/json' \
#   -H 'X-Veridact-Signature: sha256=9519693faae7420007458e2b9213460caf5bbe1d7b6c9d3b8e6d31f77ffbc192' \
#   --data-binary '{"event":"subject_check.completed","subject_check_id":"ck_a7b3f2","status":"completed","verdict":"clear","created_at":"2026-07-30T14:02:11.082Z"}'
# expected: signature_verified=True signature=sha256=... expected=sha256=...  response {"ok":true}

For end-to-end client-flow testing without consuming quota or producing real subjects — see POST /api/v1/compliance/subject-checks/sandbox, which returns the same response shape with a deterministic verdict and writes no rows.

POST /api/v1/compliance/subject-checks/sandbox

Synchronous mock verdict for client-flow testing. Identical request shape to the real subject-checks endpoint, but performs no person_checks insert, no quota increment, no async pipeline kickoff, and no rate-limit decrement — the per-token 60/hr bucket is untouched. Useful for exercising the full request/response flow (including the invalid_payload 400 path) without committing a real subject.

Request body

Same payload as POST /api/v1/compliance/subject-checks: first_name, last_name, locations[], purpose, consent_given are required; middle_name, dob, aliases[], and additional_identifiers are optional.

Successful response — 201
{
  "success": true,
  "subject_check_id": "sandbox",
  "status": "completed",
  "subject_name": "Jane Marie Reyes",
  "verdict": "clear_no_match",
  "confidence_score": 1.0,
  "created_at": "2026-07-30T14:02:11.082Z"
}

The subject_check_id is the literal string "sandbox" and verdict is fixed to "clear_no_match" on every call — use this to assert on the shape of your client wrapper without coupling to real matching results. No person_checks row, no quota increment, no rate-limit decrement.

Replace vda_your_token_here with the secret minted in /portal. POST returns 201 with a subject_check_id to poll the result against.

Rate limits & error codes

60 subject checks per hour per token, returning HTTP 429 with a standard Retry-After header when exceeded. The window slides; quotas do not reset on a fixed clock. Enterprise tiers can request a higher ceiling via the contact-sales path — the subjectChecksLimiter ceiling is keyed to the calling token, so each token in your account draws from its own bucket.

Errors are returned as a single error object with a stable code and a human-readable message. The codes below are the full set you should expect to handle:

Code HTTP status Returned by What it means
invalid_key_format 401 authenticateAccountApiKey Header missing, not Bearer-prefixed, or the token does not start with vda_.
invalid_key 401 authenticateAccountApiKey Hash not found in the live token table — typically a revoked or mistyped key.
key_expired 401 authenticateAccountApiKey Token revocation was paired with an explicit expiry timestamp; mint a new key.
invalid_payload 400 POST /subject-checks One or more required fields failed validation; the response errors array names them.
rate_limited 429 subjectChecksLimiter Hourly ceiling reached; back off using the Retry-After header.
internal_error 500 POST /subject-checks Unexpected server fault; the row was not inserted. Retry with idempotency keys.

The error.code field is the contract — machine-readable and stable across API versions. Don't pattern-match on error.message.

Compliance-tier SLA

Support windows and audit retention by tier. Common ceiling is the one your token ships with — Enterprise is contact-sales.

Tier Rate ceiling Support response Audit log retention
Essentials 60 checks / hour Email, best-effort Not applicable
Professional 60 checks / hour Priority support (24h response) Aligned with FinCEN 5-year retention
Enterprise Custom ceiling (contact sales) Custom SLA agreement Aligned with FinCEN 5-year retention

The 5-year retention default reflects FinCEN 31 CFR §1020.320 record-keeping requirements for covered financial institutions — the same window your existing screening data already meets per the security page. Custom retention windows and dedicated integration engineers are negotiated with the Enterprise agreement.

Shareable verdicts

Once a subject check completes inside your account, you can mint a read-only share URL via the /share/:token endpoint exposed in the account UI. The URL embeds a signed, expiring token — it never exposes your bearer credential, never exposes the raw subject_check_id, and can be revoked independently of the underlying screening record. Use it to send verdicts to compliance reviewers, auditors, or counter-parties who need to see results without holding their own Veridact key.

The share button lives next to each completed run in /account/checks; the API returns the same shape your UI calls internally.

Successful response — 200
{
  "subject_check_id": "ck_a7b3f2",
  "share_url": "https://veridact.solutions/share/s_a91c...",
  "expires_at": "2026-09-30T14:02:11.082Z"
}

Requesting a token

Compliance API tokens are available to accounts on the Professional and Enterprise tiers — gating is enforced at the API-key mint endpoint, and lower-tier accounts receive a clear 403. To get a token, you can either upgrade your account or sign in and mint one directly.

Existing customers: open /portal → Settings → API Keys, name the integration (e.g. prod-customer-onboarding-prod-1), and the plaintext vda_… secret will be shown exactly once. Copy it into your secret manager immediately — we cannot recover it later.

Request a token →