Skip to main content

How It Works

Webhooks let you receive automatic notifications when events occur in Veridox, such as a file analysis completing.
1

Create a webhook configuration

An organisation owner creates a webhook configuration via the API, providing a destination URL where you want to receive events.
2

Store your signing secret

Veridox provisions a signing secret (format: whsec_{32-char-alphanumeric}) and returns it once in the creation response. After that, only a prefix is shown for identification.
The secret is only shown once at creation time. Store it securely — you cannot retrieve it again.
3

Receive events

When events occur, Veridox sends a POST request to your URL with:
  • A JSON payload in the body
  • An x-vdx-signature header containing an HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret
4

Verify and process

You verify the signature on your server to confirm the request genuinely came from Veridox and hasn’t been tampered with, then process the event.

Event Types

Event typeDescription
file.enrichment.successFile analysis completed successfully
file.enrichment.failedFile analysis encountered an error

Payload Structure

When a file analysis completes successfully, Veridox delivers a payload like this:
{
  "event_type": "file.enrichment.success",
  "file_id": "019e2a1b-7c3d-7f00-8a1b-2c3d4e5f6a7b",
  "case_id": "019e2a1b-5b2c-7e00-9d8c-7b6a5f4e3d2c",
  "started_at": "2026-02-17T10:30:00.000Z",
  "completed_at": "2026-02-17T10:30:42.000Z",
  "version": "2.0.0",
  "file_info": {
    "md5": "d41d8cd98f00b204e9800998ecf8427e",
    "mimetype": "application/pdf"
  },
  "highlights": ["Metadata manipulation detected", "AI-generated content indicators found"],
  "results": {
    "overall_risk_score": "high",
    "summary": "Multiple indicators of document manipulation detected...",
    "findings": ["..."],
    "observations": ["..."],
    "risk_assessments": ["..."],
    "suggested_actions": ["..."],
    "audit_trail": ["..."],
    "search_evidence_summary": {
      "total_queries": 7,
      "total_results": 21,
      "per_module": {
        "content_verification": {
          "queries": ["..."],
          "results": ["..."],
          "cited_result_ids": ["q1.0", "q2.1"]
        }
      }
    }
  },
  "modules": [
    {
      "module_id": "content_verification",
      "module_name": "Content Verification",
      "findings": [
        {
          "alert_type": "ENTITY_NOT_VERIFIED",
          "alert_type_name": "Entity Not Verified",
          "found": true,
          "severity": "Medium",
          "severity_justification": "Organisation not present in public registry records.",
          "description": "ACME Ltd is named as the issuer but does not appear in the company registry.",
          "evidence": [
            {
              "source": "Search Evidence",
              "detail": "q1.0",
              "explanation": "Registry search returned no matching company"
            }
          ],
          "evidence_refs": ["q1.0"]
        }
      ],
      "search_evidence": {
        "queries": [
          {
            "query_id": "q1",
            "query_text": "ACME Ltd company registration",
            "answer": "No active registration found for ACME Ltd."
          }
        ],
        "results": [
          {
            "result_id": "q1.0",
            "query_id": "q1",
            "url": "https://example.com/registry/acme-ltd",
            "title": "Company search — ACME Ltd",
            "snippet": "No active company found...",
            "published_date": null,
            "last_updated": "2026-02-17"
          }
        ],
        "failures": [],
        "elapsed_ms": 1320,
        "request_count": 1
      },
      "verification_evidence": null
    }
  ],
  "artifacts": {
    "report_pdf": "reports/019e2a1b-7c3d-7f00.../report.pdf",
    "report_log": "reports/019e2a1b-7c3d-7f00.../report.log"
  },
  "analysis_metadata": {
    "...": "..."
  }
}
For file.enrichment.failed events, the payload includes an error field instead of results.
The highlights field within results is omitted when the analysis did not produce any highlights.

Module Evidence

Each entry in modules carries its findings alongside the evidence that supports them:
FieldTypeDescription
findingsarrayStructured findings. Each has an alert_type, a severity (Informational, Low, Medium, or High), a description, a severity_justification, an evidence list, and evidence_refs.
search_evidenceobject | nullAudit trail of the search and grounding queries run for this module — the queries, their results, any failures, and timing. null when the module performed no search.
verification_evidenceobject | nullReserved for registry verification check results. Currently always null.
A finding’s evidence_refs are IDs that point back into the module’s evidence so you can resolve each claim to its source:
  • IDs like q1.0 resolve against search_evidence.results (matched on result_id).
  • IDs like v1 resolve against verification_evidence.checks (reserved for future use).
The results.search_evidence_summary object is a cross-module roll-up of the same queries and results, keyed by module_id — useful for rendering an overview without walking every module.
search_evidence_summary and a query’s answer field may be null (or, for analyses produced before these fields existed, absent). Treat both as optional when parsing.

Signature Verification

The signature is computed as:
HMAC-SHA256(raw_request_body, your_webhook_secret) → hex string
You compare the x-vdx-signature header value against your own computed HMAC. Always use a timing-safe comparison to prevent timing attacks.
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";

const WEBHOOK_SECRET = "whsec_your_secret_here";

function verifySignature(body, signature) {
  if (!signature) return false;
  const expected = createHmac("sha256", WEBHOOK_SECRET)
    .update(body)
    .digest("hex");
  const sigBuf = Buffer.from(signature, "utf8");
  const expBuf = Buffer.from(expected, "utf8");
  if (sigBuf.length !== expBuf.length) return false;
  return timingSafeEqual(sigBuf, expBuf);
}

createServer(async (req, res) => {
  if (req.method !== "POST") {
    res.writeHead(404).end();
    return;
  }

  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const body = Buffer.concat(chunks);

  if (!verifySignature(body, req.headers["x-vdx-signature"])) {
    res.writeHead(401, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ error: "Invalid signature" }));
    return;
  }

  const event = JSON.parse(body.toString("utf8"));
  console.log("Verified webhook event:", event);

  // Process the event...

  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ ok: true }));
}).listen(3000, () => console.log("Webhook receiver on :3000"));

Best Practices

Store secrets securely — the signing secret is shown once at creation time and cannot be retrieved again
Always verify signatures — validate every incoming request before processing the payload
Use timing-safe comparison — use timingSafeEqual (Node.js) or hmac.compare_digest (Python) to prevent timing attacks
Return 2xx quickly — acknowledge receipt immediately and do heavy processing asynchronously
Only one active webhook configuration is allowed per organisation at a time. Creating a new configuration deactivates the previous one.