REST API
Astrari exposes a JSON-over-HTTPS REST API for everything the dashboard does — list sites and servers, query findings, trigger scans, pull reports, and receive webhooks when new findings appear. API keys are available on the Professional and Agency plans (see pricing).
Base URL: https://api.astrari.io
Format: JSON request and response bodies. Content-Type: application/json on every write request.
Encoding: UTF-8. Dates are ISO 8601 strings in UTC unless otherwise stated.
Use a Bearer-token API key in the Authorization header. API keys are issued from the dashboard at Settings → Organisation → API keys. Agency-plan customers can additionally issue per-client API keys at Clients → [client] → API keys; those keys are automatically scoped to that client's assets.
curl example
curl https://api.astrari.io/sites \ -H "Authorization: Bearer iw_live_xxxxxxxxxxxxxxxxxxxxxxxx"
Keys begin with iw_live_ followed by 32 random base64url bytes. We only store a SHA-256 hash of each key — the raw value is shown to you exactly once at creation. Lose it, rotate it.
Multi-tenancy. Every endpoint that returns data is automatically scoped to the organisation (and client, if your key is client-scoped) that owns the API key. You never need to pass an org or client identifier — it's derived from the key.
Pagination. List endpoints accept limit (max 200, default 50) and offset query parameters.
Errors. Non-2xx responses share a common shape:
error response
{
"statusCode": 404,
"code": "NOT_FOUND",
"message": "Site not found"
}Common codes: UNAUTHORIZED, FORBIDDEN, NOT_FOUND, VALIDATION_ERROR, RATE_LIMITED, PLAN_LIMIT.
/sites/sitesurl (full URL including scheme). Optional: label, scanEnabled.POST /sites — create
curl -X POST https://api.astrari.io/sites \
-H "Authorization: Bearer iw_live_…" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","label":"Marketing site"}'/sites/:id/sites/:idserverId./sites/:id/scan{ jobId } immediately; results land via subsequent GETs (or webhooks)./sites/:id/findings?status=OPEN to filter./sites/:id/servers/serversagentToken exactly once — needed for the install command./servers/:id/servers/:id/findings/servers/:id/scan{ "scanType": "full" | "trivy" | "clamav" | "rkhunter" | "maldet" | "fim" }./servers/:id/diagnose{ diagnosticId }./servers/:id/diagnostics/:diagId/servers/:id/findingsseverity = CRITICAL | HIGH | MEDIUM | LOW | INFOstatus = OPEN | RESOLVED | SUPPRESSEDcategory = e.g. WP_PLUGIN_CVE, SRV_PACKAGE_CVE (see /docs/faq for the list)siteId / serverIdlimit (max 200) / offsetGET /findings — open critical only
curl "https://api.astrari.io/findings?status=OPEN&severity=CRITICAL" \ -H "Authorization: Bearer iw_live_…"
Where vendor matching is enabled, package findings matched against vendor advisories also carry vendorPublishedAt (ISO 8601 timestamp or null: the earliest distribution publication date among the vendor advisories still unfixed for that package on that server), upstreamPublishedAt (ISO 8601 timestamp or null: Red Hat's original date where an AlmaLinux or Rocky Linux advisory cites a Red Hat advisory), vendorAdvisoryIds (array of Astrari advisory record ids, not the public ALSA-/USN- ids; the readable advisory names are in the finding detail) and inventoryAt (ISO 8601 timestamp or null: the package list the match used). They are null or empty on other findings.
/findings/:id/findings/:id{ "status": "RESOLVED" | "SUPPRESSED", "note": "…" }. Suppression survives rescans (re-detections roll up into the same suppressed finding)./reports/reportstype, title, periodStart, periodEnd, serverIds, siteIds./reports/:idstatus (PENDING / GENERATING / READY / FAILED)./reports/:id/fileRead-only feeds for copying evidence into your own audit archive, for example with a nightly job. An organisation key sees the whole organisation; a client key sees only that client's servers and that client's log review records. Vault feeds return oldest first, up to limit rows (default 500, max 1000); pass the returned nextCursor as cursor for the next page, and since (ISO date) to start from a point in time. These are the only Vault and log review routes an API key can call.
/vault/audit/attestations/vault/audit/snapshots/log-review/signoffs/log-review/evidence/log-review/evidence/:id/csv/compliance/exports/compliance/exports/:id/download.GET /vault/audit/attestations — first page from 1 October
curl "https://api.astrari.io/vault/audit/attestations?since=2026-10-01&limit=500" \ -H "Authorization: Bearer iw_live_…"
Astrari can POST a JSON digest to a URL of your choosing whenever new findings appear on an asset. Configure webhooks in the dashboard at Settings → Alert integrations → Generic webhook and route them per-asset under each server / site's alert recipients.
Verifying signatures. If you set a webhook secret when creating the integration, every request includes an X-Astrari-Signature header containing sha256=<hex digest> of the raw request body, computed with HMAC-SHA-256 keyed on the secret.
example payload
{
"org": "Allen Digital Studio",
"asset": {
"type": "site",
"label": "shop.allendigital.example",
"subLabel": null,
"url": "https://app.astrari.io/sites/abc123"
},
"scan": { "id": "scan_xyz", "completedAt": "2026-05-03T10:14:22Z" },
"findings": [
{
"id": "find_456",
"severity": "HIGH",
"category": "WP_PLUGIN_CVE",
"title": "WooCommerce 8.4.0 — CVE-2024-XXXX",
"detail": "…",
"remediation": "Upgrade WooCommerce to 8.6.1 or later",
"url": "https://app.astrari.io/findings/find_456"
}
],
"sentAt": "2026-05-03T10:14:25Z"
}verify in Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyAstrariWebhook(rawBody: Buffer, header: string, secret: string): boolean {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
// Both sides must be the same length for timingSafeEqual.
if (header.length !== expected.length) return false;
return timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}Astrari retries with exponential backoff (max 3 attempts) on 5xx responses or network errors. 4xx responses are treated as a permanent failure. Each finding fans out to each routed webhook at most once per occurrence — re-scans that re-see the same issue do not re-fire.
The API enforces 100 requests per minute, keyed on the first 20 characters of your API key (or remote IP for unauthenticated requests). Bursts above the limit return HTTP 429 with a RATE_LIMITED error code and a Retry-After header.
Request bodies are capped at 1 MB. Endpoints that accept lists (recipients, IDs) cap the array length per endpoint — you'll get a VALIDATION_ERROR if you exceed it.
The API is currently unversioned in the URL path. Backwards-incompatible changes will be communicated via the changelog at least 30 days before they ship; we'll publish a /v2 base URL when that becomes necessary so existing integrations continue to work.
Spotted something missing or unclear? Email [email protected] — we keep this page accurate.