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.
GET /api/v1/compliance/cases/:caseId/decision-history
Returns the same chronological decision records shown on the analyst case detail page. Send a UUID case id and authenticate with Authorization: Bearer vda_…. The case and its associated person check must both belong to the bearer key's account. A malformed UUID returns 400 invalid_payload; a missing or other-tenant case returns the same 404 not_found response.
curl
curl -i -X GET https://veridact.solutions/api/v1/compliance/cases/550e8400-e29b-41d4-a716-446655440000/decision-history \
-H "Authorization: Bearer vda_your_token_here"
Sample response — 200
{
"success": true,
"case_id": "550e8400-e29b-41d4-a716-446655440000",
"decision_history": [
{
"id": "automated:12345",
"type": "automated_verdict",
"timestamp": "2026-09-28T13:00:00.000Z",
"verdict": "concerning",
"actor_id": null,
"actor_name": null,
"actor_email": null,
"reason": null
},
{
"id": "enhanced_review:550e8400-e29b-41d4-a716-446655440000",
"type": "enhanced_review",
"timestamp": "2026-09-28T13:30:00.000Z",
"verdict": "escalate",
"actor_id": 44,
"actor_name": "Analyst Name",
"actor_email": "analyst@example.com",
"reason": "Additional review required"
},
{
"id": "correction:901",
"type": "re_review",
"timestamp": "2026-09-28T14:02:11.082Z",
"previous_disposition": "accepted",
"resulting_disposition": "rejected",
"actor_id": 44,
"actor_name": "Analyst Name",
"actor_email": "analyst@example.com",
"reason": "Disposition overridden",
"is_override": true
},
{
"id": "correction:902",
"type": "re_review",
"timestamp": "2026-09-28T15:02:11.082Z",
"previous_disposition": "rejected",
"resulting_disposition": "rejected",
"actor_id": null,
"actor_name": null,
"actor_email": null,
"reason": "Re-review confirmed",
"is_override": false
}
]
}
POST /api/v1/compliance/subject-checks/:id/disposition-correction
Authenticate with Authorization: Bearer vda_… to record an analyst correction for a completed subject check owned by the token's account. The account user must currently have the database-backed analyst role. The correction and audit-history entry are saved atomically. Each committed correction emits one signed event: subject_check.verdict_overridden when the effective disposition changes, or subject_check.re_reviewed when it stays the same. Subscribe to both event types to receive every correction.
Request body
// POST /api/v1/compliance/subject-checks/12345/disposition-correction
{
"disposition": "rejected", // accepted, rejected, or escalated
"reason": "Analyst review recorded" // trimmed, 1–500 characters
}
Successful response — 200
{
"success": true,
"subject_check_id": 12345,
"analyst_correction": {
"id": 901,
"previous_disposition": "accepted",
"corrected_disposition": "rejected",
"corrected_by": 77,
"reason": "Analyst review recorded",
"created_at": "2026-09-28T14:02:11.082Z"
},
"correction_history": [{
"id": 901,
"person_check_id": 12345,
"previous_disposition": "accepted",
"corrected_disposition": "rejected",
"corrected_by": 77,
"reason": "Analyst review recorded",
"created_at": "2026-09-28T14:02:11.082Z"
}]
}
Correction errors
400 invalid_payload means the path ID, disposition, or reason failed validation. 403 analyst_required means the account user lacks analyst privilege. 404 not_found covers both an unknown check and a check owned by another tenant. 409 decision_not_ready means the check has not completed.
POST /api/v1/compliance/subject-checks/:id/verdict-override
Authenticate with Authorization: Bearer vda_… to override the verdict on a completed subject check owned by the token's account. The account user must currently have the database-backed analyst role. The override and correction-history entry are saved atomically. The target must have a supported effective verdict (accepted, rejected, or escalated), and replacement_verdict must differ from it. Every successful override emits the signed subject_check.verdict_overridden lifecycle event with the recorded reason in override_context.
Request body
// POST /api/v1/compliance/subject-checks/12345/verdict-override
{
"replacement_verdict": "rejected", // accepted, rejected, or escalated; must differ from current verdict
"reason": "Analyst review confirmed a sanctions match." // trimmed, 1–500 characters
}
Successful response — 200
{
"success": true,
"subject_check_id": 12345,
"verdict_override": {
"id": 905,
"person_check_id": 12345,
"previous_disposition": "accepted",
"corrected_disposition": "rejected",
"corrected_by": 77,
"reason": "Analyst review confirmed a sanctions match.",
"created_at": "2026-10-11T14:02:11.082Z"
},
"correction_history": [{ /* newest record first; includes the saved override */ }]
}
Override event
{
"event": "subject_check.verdict_overridden",
"previous_verdict": "accepted",
"replacement_verdict": "rejected",
"override_context": {
"correction_id": 905,
"analyst_id": 77,
"reason": "Analyst review confirmed a sanctions match."
}
}
Override errors
400 invalid_payload means the ID is not a positive safe integer, replacement_verdict is missing or unsupported, or reason is missing, blank after trimming, or longer than 500 characters. 401 means bearer credentials are missing or invalid. 403 analyst_required means the account user lacks analyst privilege. 404 not_found covers both an unknown check and a check owned by another tenant. 409 decision_not_ready means the check is incomplete; 409 verdict_not_overridable means its current effective verdict is unsupported or the replacement matches it. 500 internal_error means an unexpected failure while saving the override.
POST /api/v1/compliance/subject-checks/:id/re-review
Authenticate with Authorization: Bearer vda_… to record an analyst re-review for a completed subject check owned by the token's account. The account user must currently have the database-backed analyst role. The outcome and correction-history entry are saved atomically through the same path as disposition-correction; each saved re-review emits exactly one signed event: subject_check.re_reviewed when the effective disposition is unchanged, or subject_check.verdict_overridden when it changes.
Request body
// POST /api/v1/compliance/subject-checks/12345/re-review
{
"outcome": "accepted", // accepted, rejected, or escalated
"reason": "Analyst re-review recorded" // required, trimmed, 1–500 characters
}
Successful response — 200
{
"success": true,
"subject_check_id": 12345,
"analyst_correction": {
"id": 902,
"previous_disposition": "accepted",
"corrected_disposition": "accepted",
"corrected_by": 77,
"reason": "Analyst re-review recorded",
"created_at": "2026-09-28T14:02:11.082Z"
},
"correction_history": [{ /* newest record first; same fields as analyst_correction */ }]
}
Re-review errors
400 invalid_payload means the path ID, outcome, or reason failed validation. 403 analyst_required means the bearer account user lacks analyst privilege. 404 not_found covers both an unknown check and a check owned by another tenant. 409 decision_not_ready means the check has not completed. Unexpected failures return 500 internal_error.
POST /api/v1/compliance/webhooks
Registers (or updates) an https:// callback URL for the calling token's account. The optional event_types array selects which supported Compliance API events the endpoint receives. New subscriptions default to all supported events; clients that omit this field when updating an existing subscription keep its current selection. The saved selection is returned in this response. webhook.test is an operator probe and is not selectable; it remains available from the account dashboard's test button. Veridact POSTs selected subject-check lifecycle, batch, monitoring, and manual retry events. 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",
"event_types": ["subject_check.completed", "batch_check.completed"]
}
Successful response — 200
{
"success": true,
"subscription_id": "wh_sub_a1f3...",
"url": "https://hooks.example.com/veridact/results",
"signing_secret": "whsec_3x9p...",
"event_types": ["subject_check.completed", "batch_check.completed"],
"created_at": "2026-07-30T14:02:11.082Z"
}
Supported event names: subject_check.processing, subject_check.review_required, subject_check.escalated, subject_check.completed, subject_check.failed, subject_check.re_reviewed, subject_check.verdict_overridden, subject_check.retry, batch_check.completed, monitoring.alert, monitoring.verdict_changed, and monitoring.risk_changed. The saved array may be empty. webhook.test is never part of this selectable list.
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. |
batch_check.completed | completed, failed, or partial | completed_at | Stable batch_id, counts, ordered subjects, failures, and partial-completion details. |
Every subject lifecycle payload includes subject_check_id, the tenant account_id, and its stage status. Batch summaries use batch_id as their stable receiver idempotency key and are emitted only after every input is terminal. 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.