NOVAI Pulse API
Scan a public website and get a ranked list of fixes for search (SEO), AI answers (GEO), AI agents and
security. Base URL https://novaipulse.com/api/v1. JSON in, JSON out.
Authentication
Send your key in a header: Authorization: Bearer npk_... or X-API-Key: npk_.... Keep it on
the server or in CI secrets, never in browser code. Get a key.
Start a scan
POST /api/v1/scans
{
"url": "https://yoursite.com", // required, a public http(s) address
"depth": "quick", // "quick" (default) or "full"
"previous_scan_id": "", // optional: the scan you fixed, for a diff
"site_id": "", // optional: your own id for the site
"callback_url": "" // optional: https URL we POST to when done
}
Answers 202 with scan_id, status_url and poll_after_seconds.
Read the result
GET /api/v1/scans/{scan_id}
While status is queued or running, poll again after
poll_after_seconds. When it is done you get score (overall and per area),
fixes, advice, next_step, a ready resubmit body and, on a re-scan,
diff (fixed, still open, regressed, new).
A fix
| Field | What it is |
|---|---|
fix_id | Stable id for this issue on this site, the same across re-scans. |
priority | 1 is the fix to do first (impact for effort). |
code | The check, for example DEV_SECRET_IN_CLIENT or missing_canonical. |
channel | seo, geo, agentic, security, access or compliance. |
severity | critical, high, medium, low or info. |
title, why, how | What is wrong, why it matters, the steps to fix it. |
affected_urls, evidence | Where we saw it and what we measured. |
expected_points, effort | Our estimate of the score gain, and low, medium or high effort. |
example_patch | When we can: a draft of the file or markup, built from the site's own content. |
verify | How the next scan decides that the fix is done. |
Tell us what you did
POST /api/v1/scans/{scan_id}/feedback
{"items": [{"fix_id": "...", "status": "applied"},
{"fix_id": "...", "status": "not_applicable", "note": "We have no physical address"}]}
Status is applied, rejected, not_applicable or cannot (the last
three need a note). Fixes you rule out drop to the bottom on later scans. Then deploy and POST a new scan with
previous_scan_id to verify.
Usage, lessons and billing
GET /api/v1/usage plan, scans used and left this month, when it resets
GET /api/v1/lessons which fixes moved the score on your sites
POST /api/v1/billing-portal a link to change plan, card or cancel
Limits
| Plan | Scans a month | Price |
|---|---|---|
| Developer | 500 | $19 or €19 a month |
| Pro | 5,000 | $79 or €79 a month |
On every plan: at most 30 scans an hour per key and 10 an hour for one site.
A refused request answers 429 with Retry-After.
Errors
| Status | error | Meaning |
|---|---|---|
| 400 | bad_url | Not a public http(s) address. |
| 401 | missing_key, bad_key | No key, or not a valid one. |
| 402 | plan_inactive | The payment failed or the plan was cancelled. |
| 404 | scan_not_found | No scan with that id for this key. |
| 429 | quota_exceeded, rate_limited | Monthly quota used, or too many scans this hour. |
| 503 | busy | Many scans right now. Retry after the given seconds. |
Webhook
With callback_url we POST {"event": "validation.run.finished", "scan_id": "...", "status": "done",
"status_url": "/api/v1/scans/..."} when the scan ends. Treat it as a nudge: read the result from
status_url with your key.
Example: fail CI on a critical issue
ID=$(curl -s -X POST https://novaipulse.com/api/v1/scans -H "Authorization: Bearer $NOVAI_API_KEY" \
-H "Content-Type: application/json" -d "{\"url\": \"$PREVIEW_URL\"}" | jq -r .scan_id)
until [ "$(curl -s https://novaipulse.com/api/v1/scans/$ID -H "Authorization: Bearer $NOVAI_API_KEY" | jq -r .status)" = done ]; do sleep 10; done
curl -s https://novaipulse.com/api/v1/scans/$ID -H "Authorization: Bearer $NOVAI_API_KEY" \
| jq -e '[.fixes[] | select(.severity == "critical")] | length == 0'
Example: JavaScript
const res = await fetch("https://novaipulse.com/api/v1/scans", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.NOVAI_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://yoursite.com" }),
});
const { scan_id, poll_after_seconds } = await res.json();