Skip to main content

Developer API

REST API

Authenticate with your API key, submit a URL, get results via webhook or polling. Available on the 250-scan plan and up.

Base URL

http
https://www.seolint.dev/api/v1

Authentication

Pass your API key as a Bearer token in every request. Generate your key from the API dashboard.

bash
curl -X POST https://www.seolint.dev/api/v1/scan \
  -H "Authorization: Bearer sl_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'
401Missing or invalid API key
403API key valid but no active subscription

Rate limits

Limits mirror your plan's monthly scan quota. There's no per-second rate limit, since each scan takes 15 to 45 seconds and concurrency caps naturally. The REST API and MCP server share the same quota per API key.

PlanScans / monthConcurrent
Free preview11
SEOLint / $791005
CustomNegotiatedNegotiated
When the monthly cap is reached, the API returns HTTP 429 with a Retry-After header set to the seconds remaining until quota resets. Back off and retry after that delay instead of hammering the endpoint.
POST

/scan

Submit a URL for scanning. Returns immediately. Results arrive via webhook or polling.

bash
curl -X POST https://www.seolint.dev/api/v1/scan \
  -H "Authorization: Bearer sl_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'

Response

json
{
  "scanId": "3f2a1b4c-...",
  "pollUrl": "https://www.seolint.dev/api/v1/scan/3f2a1b4c-...",
  "reportUrl": "https://www.seolint.dev/scan/3f2a1b4c-..."
}
GET

/scan/:id

Poll until status is complete. Recommended interval: 3 seconds.

bash
curl https://www.seolint.dev/api/v1/scan/3f2a1b4c-...

Response (complete)

json
{
  "status": "complete",
  "issues": [
    {
      "id": "missing-title",
      "category": "seo",
      "severity": "critical",
      "title": "Missing page title",
      "description": "The page has no <title> tag...",
      "fix": "Add a unique <title> tag under 60 characters."
    }
  ],
  "markdown": "# Website Audit: https://example.com\n\n..."
}
GET

/scan/:id/markdown

Returns the report as a raw .md file. Ideal for piping into AI agents, Claude, or Cursor.

bash
curl https://www.seolint.dev/api/v1/scan/3f2a1b4c-.../markdown

Memory & site intelligence

Every scan builds a persistent picture of the site: goal, audience, sitemap structure, and content gaps. These endpoints expose that memory so Claude (or your own agent) can use it without re-scanning.

GET/site-intelligence?domain=example.com

Full intelligence picture for a domain. Start every SEO session with this. Returns site profile (goal, ICP, niche), sitemap breakdown, cross-page patterns, scan coverage, and a summary of missing pages.

bash
curl "https://www.seolint.dev/api/v1/site-intelligence?domain=example.com" \
  -H "Authorization: Bearer sl_your_api_key"

Response

json
{
  "domain": "example.com",
  "profile": {
    "goal": "Help developers ship faster with better tooling",
    "icp": "Solo developers and small teams using Claude daily",
    "primary_keyword": "developer SEO tools",
    "niche": "developer tooling",
    "business_model": "B2B",
    "business_type": "SaaS"
  },
  "sitemap": {
    "total_urls": 84,
    "page_type_counts": { "blog_post": 20, "homepage": 1, "pricing": 1 },
    "haiku_insights": "Strong blog presence but no comparison pages...",
    "unscanned_urls": ["/blog/seo-checklist", "/blog/fix-lcp"]
  },
  "page_suggestions": [
    { "slug": "/vs/ahrefs", "title": "SEOLint vs Ahrefs", "target_keyword": "ahrefs alternative" }
  ],
  "markdown": "# Site Intelligence: example.com\n\n..."
}
The markdown field is formatted for Claude. Paste it directly into your conversation for instant context.
GET/suggest-pages?domain=example.com

Missing page suggestions generated from sitemap analysis and site profile. Each suggestion includes a copy-paste brief for creating the page in Claude or Cursor. Generated on first scan, updated automatically when new scan data is available.

bash
curl "https://www.seolint.dev/api/v1/suggest-pages?domain=example.com" \
  -H "Authorization: Bearer sl_your_api_key"

Response

json
{
  "domain": "example.com",
  "suggestions": [
    {
      "slug": "/blog/fix-missing-h1",
      "title": "How to Fix Missing H1 Tags",
      "cluster": "technical SEO",
      "intent": "informational",
      "target_keyword": "fix missing h1 tag",
      "reason": "You cover H1 theory but have no how-to content for fixing it",
      "brief": "Write a developer-focused post titled 'How to Fix Missing H1 Tags'..."
    }
  ],
  "total_sitemap_urls": 84,
  "analyzed_at": "2026-04-05T10:00:00.000Z",
  "markdown": "# Page Suggestions: example.com\n\n..."
}
Use the brief field. Paste it directly into Claude or Cursor to create the page immediately.

Additional endpoints

GET/open-issues?url=https://example.comAll unresolved issues for a URL — no re-scan needed
GET/history?url=https://example.comScan history with NEW / FIXED / PERSISTING diffs
GET/site-status?url=https://example.comTrend + rescan recommendation for a URL
GET/sitesAll domains you've scanned with health summary
POST/scan/:id/resolveMark issue IDs as fixed: { issueIds: ["missing-title"] }

Webhooks

Set a webhook URL in your API dashboard and we'll POST results to your endpoint when each scan finishes. No polling needed.

Events

scan.completedScan finished successfully
scan.failedScan encountered an error

Payload

json
{
  "event": "scan.completed",
  "scan_id": "3f2a1b4c-...",
  "url": "https://example.com",
  "status": "complete",
  "issue_count": 8,
  "critical_count": 2,
  "report_url": "https://www.seolint.dev/scan/3f2a1b4c-...",
  "timestamp": "2026-03-30T12:00:00.000Z"
}

Verifying signatures

Every request includes an X-SEOLint-Signature header. Verify it with your signing secret (shown in the API dashboard) to confirm the request came from us.

webhook-verify.js
const crypto = require('crypto')

function isValidWebhook(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)          // rawBody must be the raw string, not parsed JSON
    .digest('hex')
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )
}

// Express
app.post('/webhooks/scanner', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-seolint-signature']
  if (!isValidWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature')
  }
  const event = JSON.parse(req.body)
  console.log(event.event, event.url, event.critical_count)
  res.sendStatus(200)
})

Read the raw body first

Always read the raw request body before parsing JSON. Parsing then re-stringifying changes the byte sequence and breaks the HMAC check.

Status codes

200OK: request succeeded
202Accepted: scan not yet complete (markdown endpoint)
400Bad request: missing or invalid url
401Unauthorized: missing or invalid API key
403Forbidden: no active subscription
404Not found: scan ID does not exist
429Too many requests: monthly scan quota exceeded — see Retry-After
500Server error: scan failed to start