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 withPOST /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
httpsand resolve to public addresses. To allowhttpor 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.