REST API v1: API keys, authentication and endpoints
How to create an API key, send the Authorization header, and call every v1 endpoint, with parameters, the response envelope and error codes.
Open this screen in the panelThe REST API v1 lets you do most of your panel work from code: list sites, start crawls, pull issues, open AI fixes, and manage Pixel rules, keywords and content. Keys belong to an organization, and a key only ever sees its own organization's data.
Prerequisites
- Only the organization's owner or an admin can create and revoke keys. Other roles don't see the API keys tab.
- API access is included in the Pro and Agency plans. If your plan doesn't include it, the tab shows a notice with a See plans link.
Creating an API key
- Open Settings → API keys.
- In Key name, describe where the key will be used (at least 2 characters, e.g. "CI").
- If the key needs to start crawls or create fixes, rules, keywords or content, tick write scope. Leave it unticked for a read-only key.
- Click Create key.
- Copy the key that starts with
sa_live_right away with Copy key. It is never shown again; the list only keeps its Prefix.
An organization can have at most 20 active keys. If a key leaks, revoke it immediately with the trash icon (Revoke) in the list. Requests using a revoked key get 401 straight away.
Authentication and base URL
- Base URL:
https://seoasist.com/api/v1 - Header:
Authorization: Bearer sa_live_… - An
?api_key=sa_live_…query parameter is also accepted. Keys in URLs tend to end up in logs, so use it for testing only.
Scopes: a read key can call every GET endpoint. The write scope unlocks POST and DELETE requests. A write request with a read-only key returns 403.
curl -H "Authorization: Bearer sa_live_..." \
https://seoasist.com/api/v1/sites
Response envelope
Every successful JSON response looks like this:
{ "ok": true, "data": {}, "ms": 42 }
data holds the endpoint's result and ms is the request time in milliseconds. The one exception is llms.txt, which returns plain text (text/plain). Errors return { "ok": false, "error": "<code>", "message": "…" }.
Endpoints
All paths are relative to the base URL. {id} is a site ID, which you get from GET /sites.
| Method | Path | Scope | What it does |
|---|---|---|---|
| GET | /me | read | The key's organization and scopes: orgId, scopes |
| GET | /sites | read | Lists sites: id, name, domain, url, healthScore, lastCrawlAt, gscProperty, ga4PropertyId, repo, pixelKey … |
| GET | /sites/{id} | read | Site fields plus lastCrawl (id, pages, finishedAt), issueTotals and issue groups (code, severity, title, count) |
| POST | /sites/{id}/crawl | write | Starts a crawl; returns { crawlId, status: "queued" } |
| GET | /sites/{id}/issues | read | Grouped issues (totals, groups). With ?code= you get that group plus sample urls; ?limit= defaults to 200, max 1000 |
| POST | /sites/{id}/fixes | write | Opens an AI fix (PR job). Body: issueCode or instructions (one is required), optional model. Returns { jobId, status: "queued" } |
| GET | /sites/{id}/fixes | read | The latest 50 fix jobs: status, model, prUrl, summary, error … |
| GET | /sites/{id}/pixel/rules | read | Pixel rules |
| POST | /sites/{id}/pixel/rules | write | Adds a rule. Body: kind, matchType (default exact), matchPath (default /), payload, optional note |
| DELETE | /sites/{id}/pixel/rules/{ruleId} | write | Deletes a rule; returns { deleted: true } |
| GET | /sites/{id}/keywords | read | avgPosition, buckets and keywords (position, change, best, url, volume, kd, intent, aiOverview, aiCited …) |
| POST | /sites/{id}/keywords | write | Body: { keywords: string[] } (up to 200 per request). Returns { added }; existing keywords are skipped |
| GET | /sites/{id}/content | read | The latest 100 content items; filter with ?status= |
| POST | /sites/{id}/content | write | Creates a blog idea. Body: title (required), targetKeywords?, angle?, generate? (true queues the writing job) |
| GET | /sites/{id}/content/{itemId} | read | One content item with all fields (including HTML) |
| POST | /sites/{id}/content/{itemId}/approve | write | Approves an item. With scheduledFor (YYYY-MM-DD) the status becomes scheduled |
| POST | /sites/{id}/content/{itemId}/publish | write | Queues publishing. Body: channel (wordpress, github, manual), live? |
| GET | /sites/{id}/geo | read | AI visibility summary; ?days= defaults to 90 |
| GET | /sites/{id}/monitors | read | uptime (30 days), index counts and changes30d |
| GET | /sites/{id}/ai-access | read | The last AI bot access check: checkedAt, result, policy |
| GET | /sites/{id}/llms.txt | read | The generated llms.txt as plain text |
| GET | /notifications | read | { unread, items }; ?unread=1 returns unread only, ?limit= defaults to 50 |
| POST | /notifications/read | write | Body: { ids?: string[] }; without ids, everything is marked as read |
| GET | /reports | read | The latest 50 reports with their public url; filter with ?siteId= |
Content statuses: idea, planned, generating, draft, review, approved, scheduled, published, failed, archived. Pixel kind values: title, meta_description, canonical, robots, h1, img_alt, og, json_ld, hreflang, redirect, lang, attr. matchType: exact, prefix, regex, all.
Examples
Start an AI fix:
curl -X POST https://seoasist.com/api/v1/sites/SITE_ID/fixes \
-H "Authorization: Bearer sa_live_..." \
-H "Content-Type: application/json" \
-d '{"issueCode":"missing_meta_description"}'
Add keywords to tracking:
curl -X POST https://seoasist.com/api/v1/sites/SITE_ID/keywords \
-H "Authorization: Bearer sa_live_..." \
-H "Content-Type: application/json" \
-d '{"keywords":["seo tool","site audit"]}'
Crawls and fixes run as queued jobs. Follow their progress through the matching GET endpoint (/sites/{id}, /sites/{id}/fixes).
Error codes
| HTTP | error | When |
|---|---|---|
| 400 | bad_request | A body field is missing or invalid (e.g. title, kind, keywords[]) |
| 401 | unauthorized | The key is missing, invalid or revoked |
| 403 | forbidden | A write request with a read-only key |
| 404 | not_found | The site or record doesn't exist or belongs to another organization, or the endpoint is unknown |
| 409 | conflict | For fixes: the site has no GitHub repository, GitHub isn't connected, or the AI model can't be used |
| 500 | internal | Unexpected error; message has the details |
FAQ
I lost my key. Can I see it again?
No. Keys are stored only as a hash. Revoke the old one and create a new key.
Can I use the same key in an MCP client?
Yes. The MCP server view under the API keys tab shows a ready-made config for Claude Desktop, Claude Code and Cursor. Put the key in the SEOASIST_API_KEY variable; the MCP tools call the same endpoints.
Can I call the API straight from a browser?
The API returns an Access-Control-Allow-Origin: * header. Still, never embed a key in a public page: anyone who sees it can access your organization's data.
Related articles
- Accept audit issues as intentional and fix them automaticallyHow to mark issues you left on purpose as Intentional, undo that, and fix issues automatically with rules or a GitHub pull request.
- Plans, the monthly page quota and included SEOAsist AI callsThe limits of the Free, Starter, Pro and Agency plans, how the monthly crawl quota and included SEOAsist AI calls are counted, and where to track your usage.
- Site API reference (seoasist-site-api/1)The complete SEOAsist Site API spec: authentication and HMAC signing, the four endpoints, rule kinds and a summary of the ready-made Next.js/PHP samples.
- Content studio: suggestions, blog, page updates and publishingGet title/meta suggestions from Search Console opportunities, write blog posts with AI, expand existing pages and publish to WordPress, the Site API, GitHub or Shopify.