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
| Event | Status | Event timestamp | Decision context |
subject_check.processing | running | processing_at | Check and owning account_id. |
subject_check.review_required | needs_review | review_required_at | Inconclusive result, confidence, finding counts, source coverage, and review reason. |
subject_check.escalated | escalated | escalated_at | Investigation, triggering tiers and scores, requested scope, and priority. |
subject_check.completed | completed | completed_at | Existing overall_result, confidence_level, and findings_count. |
subject_check.failed | failed | failed_at | Existing 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.