ReferenceUpdated: 2026-10-16

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 panel

The 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

  1. Open Settings → API keys.
  2. In Key name, describe where the key will be used (at least 2 characters, e.g. "CI").
  3. 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.
  4. Click Create key.
  5. Copy the key that starts with sa_live_ right away with Copy key. It is never shown again; the list only keeps its Prefix.
warning

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.

MethodPathScopeWhat it does
GET/mereadThe key's organization and scopes: orgId, scopes
GET/sitesreadLists sites: id, name, domain, url, healthScore, lastCrawlAt, gscProperty, ga4PropertyId, repo, pixelKey …
GET/sites/{id}readSite fields plus lastCrawl (id, pages, finishedAt), issueTotals and issue groups (code, severity, title, count)
POST/sites/{id}/crawlwriteStarts a crawl; returns { crawlId, status: "queued" }
GET/sites/{id}/issuesreadGrouped issues (totals, groups). With ?code= you get that group plus sample urls; ?limit= defaults to 200, max 1000
POST/sites/{id}/fixeswriteOpens an AI fix (PR job). Body: issueCode or instructions (one is required), optional model. Returns { jobId, status: "queued" }
GET/sites/{id}/fixesreadThe latest 50 fix jobs: status, model, prUrl, summary, error …
GET/sites/{id}/pixel/rulesreadPixel rules
POST/sites/{id}/pixel/ruleswriteAdds a rule. Body: kind, matchType (default exact), matchPath (default /), payload, optional note
DELETE/sites/{id}/pixel/rules/{ruleId}writeDeletes a rule; returns { deleted: true }
GET/sites/{id}/keywordsreadavgPosition, buckets and keywords (position, change, best, url, volume, kd, intent, aiOverview, aiCited …)
POST/sites/{id}/keywordswriteBody: { keywords: string[] } (up to 200 per request). Returns { added }; existing keywords are skipped
GET/sites/{id}/contentreadThe latest 100 content items; filter with ?status=
POST/sites/{id}/contentwriteCreates a blog idea. Body: title (required), targetKeywords?, angle?, generate? (true queues the writing job)
GET/sites/{id}/content/{itemId}readOne content item with all fields (including HTML)
POST/sites/{id}/content/{itemId}/approvewriteApproves an item. With scheduledFor (YYYY-MM-DD) the status becomes scheduled
POST/sites/{id}/content/{itemId}/publishwriteQueues publishing. Body: channel (wordpress, github, manual), live?
GET/sites/{id}/georeadAI visibility summary; ?days= defaults to 90
GET/sites/{id}/monitorsreaduptime (30 days), index counts and changes30d
GET/sites/{id}/ai-accessreadThe last AI bot access check: checkedAt, result, policy
GET/sites/{id}/llms.txtreadThe generated llms.txt as plain text
GET/notificationsread{ unread, items }; ?unread=1 returns unread only, ?limit= defaults to 50
POST/notifications/readwriteBody: { ids?: string[] }; without ids, everything is marked as read
GET/reportsreadThe 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

HTTPerrorWhen
400bad_requestA body field is missing or invalid (e.g. title, kind, keywords[])
401unauthorizedThe key is missing, invalid or revoked
403forbiddenA write request with a read-only key
404not_foundThe site or record doesn't exist or belongs to another organization, or the endpoint is unknown
409conflictFor fixes: the site has no GitHub repository, GitHub isn't connected, or the AI model can't be used
500internalUnexpected 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.

Was this article helpful?
Didn't find what you need?
Describe the problem and our team will reply by email.
Contact us