Webhooks and REST API

Webhooks

Settings → Webhooks (org admins) creates a webhook for every project of the organisation. A webhook for one project only is created through the API, with POST /api/v0/webhooks and projectId. You choose the events:

Event Sent when
analysis.completed an analysis succeeded
gate.status_changed a branch’s gate status differs from its previous analysis. This includes the first analysis, and a change caused by an issue status change

The payload is the analysis as GET /api/v0/analyses/{id} returns it: status, revision, gate status and result, engines and warnings. It also carries project (id, key, name) and branch (id, kind, name, isMain), and, for gate.status_changed, previousGateStatus.

Every request is a POST with a JSON body and these headers:

Header Value
X-Qualor-Event the event name
X-Qualor-Delivery the delivery id. Deliveries are at-least-once, so deduplicate on it
X-Qualor-Timestamp Unix seconds of this attempt
X-Qualor-Signature sha256= + hex HMAC-SHA256 of "<timestamp>.<raw body>", keyed with the webhook’s secret

Verify the signature over the raw body, and reject old timestamps:

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, headers, secret, maxAgeSeconds = 300) {
  const ts = headers['x-qualor-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > maxAgeSeconds) return false;
  const expected = 'sha256=' + createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  const given = String(headers['x-qualor-signature'] ?? '');
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
  • Qualor generates the secret (whsec_…) and shows it once, when the webhook is created. Replace it with POST /api/v0/webhooks/{id}/regenerate-secret.

  • Each attempt has 10 s to complete. A delivery gets 7 attempts with exponential backoff (1, 2, 4, 8, 16, 32 minutes). Only a 2xx answer counts as success, and redirects are not followed.

  • Recent deliveries in the UI shows each delivery’s status, response code and the first 1 KiB of the answer. Deliveries are kept 30 days. Resend one with POST /api/v0/webhooks/{id}/deliveries/{deliveryId}/redeliver.

  • Webhook URLs must be https and resolve to public addresses. To allow http or internal receivers (a chat bot on the intranet, say), an operator sets the instance setting in PostgreSQL:

    INSERT INTO instance_settings (key, value) VALUES ('webhooks', '{"allowInternalHosts": true, "allowHttp": false}')
    ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now();

Typical uses: post gate failures on main to Slack or Teams, open a ticket when the security rating drops, or feed a dashboard.

REST API

The API lives under /api/v0. It is JSON over HTTPS, described by OpenAPI 3.1 in server/openapi.json. Authenticate with Authorization: Bearer <token>. Version 0 may still change between releases.

export QUALOR_URL=https://qualor.example.com QUALOR_TOKEN=qlr_pat_…
q() { curl -fsS -H "Authorization: Bearer $QUALOR_TOKEN" -H 'Content-Type: application/json' "$@"; }

q "$QUALOR_URL/api/v0/projects?q=payments"                        # find projects
q "$QUALOR_URL/api/v0/projects/by-key?key=acme/payments-api"      # one project, with its main branch
q "$QUALOR_URL/api/v0/projects/<id>/branches"                     # branches and MRs with gate status
q "$QUALOR_URL/api/v0/branches/<id>/measures?metrics=coverage,ncloc"
q "$QUALOR_URL/api/v0/issues?branchId=<id>&severity=blocker&severity=high&inNewCode=true"
Area Endpoints
System GET /healthz, GET /readyz, GET /api/v0/system/info
Projects GET/POST /projects, GET/PATCH/DELETE /projects/{id}, GET /projects/by-key, POST/GET/DELETE /projects/{id}/tokens
Branches and analyses GET /projects/{id}/branches, GET /branches/{id}/analyses, GET /analyses/{id}, DELETE /branches/{id}
Measures GET /branches/{id}/measures, GET /branches/{id}/measures/history, GET /branches/{id}/files, GET /branches/{id}/file?path=
Issues GET /issues, GET /issues/{id}, POST /issues/{id}/transition, POST /issues/bulk-transition, PATCH /issues/{id} (severity), GET /issues/{id}/changelog
Rules, profiles, gates GET /rules, /quality-profiles…, /quality-gates… (conditions, copy, set-default), GET /metrics
Users and organisations GET/POST/PATCH /users, GET /organizations, …/members
SCM GET/POST/PATCH/DELETE /scm-connections, POST /scm-connections/{id}/test
Webhooks /webhooks…, /webhooks/{id}/deliveries, …/redeliver, …/regenerate-secret

Errors are application/problem+json with a stable code, such as PROJECT_NOT_FOUND, INSUFFICIENT_SCOPE or INVALID_TRANSITION. The API answers 404 for resources you cannot see, and 403 for resources you can see but not change. The 403 carries a code that says why.

Example: mark issues as false positives in bulk:

q -X POST "$QUALOR_URL/api/v0/issues/bulk-transition" \
  -d '{"ids":["<issue id>","<issue id>"],"to":"false_positive","comment":"Generated code, see ADR-12"}'

Full specification: docs/spec/api.md.