API reference

Webhooks

Get a signed POST when a scan ends: events, payloads, retries and checking the signature.

A webhook tells your server when a scan ends, so you don't have to poll. ValiqAI sends a signed POST to your address with what happened. Paths are under https://www.valiqai.com/api/v1; webhooks can also be managed in the app under Integrations › Webhooks.

Webhooks in ValiqAI: one endpoint, sent when a scan finishes or couldn't finish, last delivered two hours ago.
Each webhook shows its last delivery.

Events

EventSent when
scan.completedA scan finished, whatever it found. data.status is passed, failed, warning or partial.
scan.failedA scan couldn't finish. data.error says why.
scan.cancelledA scan was stopped, from the app, the API or the CLI.

scan.started is accepted when you subscribe, for older integrations, but isn't sent.

A webhook gets events for the scans of its workspace: a team webhook, for the team's scans while the person who added it is on the team; a personal webhook, for every scan you start.

POST /webhooks

Adds a webhook. Scope webhooks.

JSON
{ "url": "https://example.com/hooks/valiqai", "events": ["scan.completed", "scan.failed"] }

events defaults to ["scan.completed"]. A team key's webhooks belong to its team; a personal key can add a team webhook with organizationId if you're an owner or admin there.

JSON
{
  "webhook": {
    "id": "cmw1…",
    "url": "https://example.com/hooks/valiqai",
    "events": ["scan.completed", "scan.failed"],
    "secret": "9f2c…",
    "active": true
  },
  "message": "Webhook created successfully. Save the secret for signature verification."
}

The secret is shown only here. Store it, and use it to check each delivery's signature.

Statuserror
400Webhook URL is required
400Invalid URL format, or Invalid URL for a private or internal address
400Invalid events: <list>. Valid events: scan.completed, scan.failed, scan.cancelled, scan.started
403This is a team key; it can only add webhooks to its own team
403Only team owners and admins can add a team webhook
429Maximum 10 webhooks per user (per team, for a team key)

GET /webhooks

Lists webhooks, oldest first, with how deliveries are going. Scope webhooks.

JSON
{
  "webhooks": [
    {
      "id": "cmw1…",
      "url": "https://example.com/hooks/valiqai",
      "events": ["scan.completed"],
      "active": true,
      "organizationId": "org_8f2…",
      "createdAt": "2026-10-20T12:00:00.000Z",
      "lastDeliveryAt": "2026-10-20T13:15:16.000Z",
      "lastStatus": 200,
      "failureCount": 0
    }
  ]
}

DELETE /webhooks

Removes one: DELETE /webhooks?id=<id>. Scope webhooks. Answers { "success": true }, or 404 Webhook not found.

Changing a webhook's events, turning it off and rotating its secret are in the app, under Integrations › Webhooks.

Deliveries

Each delivery is a POST with a JSON body:

JSON
{
  "id": "7f1d2c9e-0a3b-4f1e-9c2d-5b6a7e8f9012",
  "event": "scan.completed",
  "timestamp": "2026-10-20T13:15:16.000Z",
  "organizationId": "org_8f2…",
  "data": {
    "id": "cmh2x8k1p0",
    "event": "scan.completed",
    "status": "failed",
    "url": "https://staging.example.com",
    "score": 64,
    "findingsCount": 3,
    "pollUrl": "/api/v1/scans/cmh2x8k1p0"
  }
}
  • organizationId is the workspace the scan ran in.
  • score and findingsCount come with scan.completed; error comes with scan.failed and scan.cancelled. findingsCount counts blockers, high and medium findings. For the problems themselves, GET the scan.
  • id is the delivery's id, the same on every retry: use it to ignore a repeat.
HeaderWhat it is
X-ValiqAI-EventThe event, like scan.completed.
X-ValiqAI-Delivery-IDThe delivery's id, the same as the body's id.
X-ValiqAI-Delivery-Attempt1, 2 or 3.
X-ValiqAI-Signaturet=<unix seconds>,v1=<signature>. See below.
User-AgentValiqAI-Webhook/1.0

Retries. A delivery succeeds when your server answers 2xx within 10 seconds. Otherwise it's tried again 30 seconds later, then 2 minutes after that: three attempts in all. Redirects aren't followed, so answer from the address itself. After 20 deliveries in a row fail, the webhook is turned off; turn it back on in the app.

Check the signature

The signature is an HMAC-SHA256, keyed with your webhook's secret, of the timestamp, a dot, and the raw request body. Check it on the raw body, before parsing it, and refuse a timestamp more than 5 minutes old:

verify.js
import crypto from 'node:crypto';

/** True when a delivery is from ValiqAI. `rawBody` is the request body as received. */
export function verifyValiqaiSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Each retry is signed again with a new timestamp, over the same body.

Per-scan callbacks

POST /scans also takes a webhook address for that one scan. When the scan ends, the address gets a POST with the data object above on its own, with the same headers and retries. It's signed only when the address is also one of your active webhooks; otherwise it's sent unsigned, so prefer a webhook.