Skip to main content

SEOLint has shut down. Read the post-mortem

Developer API

REST API

Authenticated with an API key, submitted a URL, returned results via webhook or polling. Retired in July 2026: every endpoint below now answers 410 Gone.

This API is retired

SEOLint shut down in July 2026 and the database is switched off. Every endpoint documented below responds 410 Gone and no new API keys are issued. The reference is kept as a record of how the API worked. Read the post-mortem.

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