Provisioning API
Create and manage projects programmatically
An account API key reaches every project you own and can create new ones. It can configure a project, run its content pipeline and site scans, apply fixes and publish — over REST, and over MCP for AI assistants. Some dashboard actions stay signed-in only; they are listed under What no key can do below.
Create a key under Account → API keys, choosing the permissions the integration needs. The raw key is shown once — only its SHA-256 hash is stored, so it cannot be recovered afterwards. Keys are prefixed vsak_, which leaked-credential scanners recognise the way they do ghp_.
curl -H "x-api-key: vsak_..." https://api.vectraseo.com/api/v1/account/meOnboard a whole project in one call
POST /api/v1/onboarding takes an account from nothing to a configured, monitored, scheduled project: the project record, content brief, generation policy, competitors, site-health monitor and content schedule — together.
curl -X POST https://api.vectraseo.com/api/v1/onboarding \
-H "x-api-key: vsak_..." -H "content-type: application/json" \
-d '{
"project": {
"business_name": "Acme Robotics",
"business_url": "https://acmerobotics.com",
"industry": "Industrial automation",
"target_audience": "Plant managers at mid-size manufacturers"
},
"competitors": ["https://competitor-a.com"],
"monitor": { "schedule": "daily" },
"schedule": { "interval_days": 7, "posts_per_run": 2 }
}' It answers 202 with a project_id you can use immediately and a job_id to poll at GET /api/v1/onboarding/{job_id}. It is asynchronous because creating a monitor fetches your sitemap, measured at 82.6s on a real sitemap index — well past the gateway's 29s ceiling. The project row itself is written synchronously, so a 202 means the project genuinely exists and a 4xx means nothing was created.
Every step is fault-isolated and reports its own outcome: a sitemap that 404s never costs you the schedule that would have been written after it. A run with failed steps still reports completed — re-apply just those steps with the individual endpoints. Re-running onboarding to retry would create a second project.
Nothing costs money unless you ask
analyze_competitors, run_initial_scan and start_content_run are the only paths that spend model budget or plan quota, and all three default to false. When you do set one, the plan check happens up front — you get a synchronous 403, not a surprise on the invoice.
Permissions
Scopes are chosen when a key is created and are fixed afterwards. Grant the least the integration needs; to change them, create a new key and revoke the old one.
projects:read comes with any write scope, because every write path reads the project first to authorise it.
Restrict a key to some projects
A key reaches every project on your account unless you create it with Only selected projects (or project_ids in POST /api/v1/account/api-keys). A restricted key works only on routes for those projects and their jobs, sees only those projects in GET /account/projects, and gets 403 for any other project, for creating a project, and for any route that names no project. Like scopes, the list is fixed at creation. Use it for a key that lives on a server for one site, so a leak cannot reach your other projects.
What no key can do
- Delete a project. Deletion here is a real purge, not a reversible hide, so it stays a signed-in action in the dashboard. There is no scope for it.
- Delete a site-health monitor. The non-destructive equivalent is programmatic instead — disable it. The deletes a key can make remove configuration, never content:
projects:writeremoves a competitor, an AI-visibility monitor or a tracked rank keyword (each can be added back) and can switch the content schedule off, andgaps:deleteremoves a suggested topic, whichPOST /gaps/identifyrebuilds. - Delete or archive a post. Archiving is deletion on a delay — the hourly sweeper purges archived rows after five days — so both stay signed-in actions.
- Manage API keys. Minting and revoking needs a session. Otherwise one leaked key could issue itself a wider one, and rotating the compromised key would not evict the attacker.
- Reveal stored CMS credentials, touch billing, or reach another tenant. A project you do not own returns
404, so a key cannot probe for project ids. - Connect or disconnect Google. Search Console and GA4 connection (OAuth consent, property choice, disconnect), and Indexing API credentials need a signed-in session. So do a post's repair actions — export, regenerate images, repair metadata, retry the truth check, remove unconfirmed claims, advance phase, replay, unarchive — and title recommendations.
Push first-party traffic
No Google Analytics or Search Console? Push your own aggregated traffic instead, typically a nightly rollup of your CDN access logs. Use a key with only traffic:write and send one UTC day per request to POST /api/v1/projects/{project_id}/traffic/daily. The fastest start is the project's Search tab: it shows this project's endpoint, a Create a key link with traffic:write already ticked and the key restricted to that project, an example request, and when the last day arrived. The data appears on the project card as "Visits (site logs)" when GA4 has no data, counted up to the newest day you sent, in GET /traffic/overview and GET /traffic/pages, and as context on each recommendation. Flag links between your own pages with self_referrer: true so a reader clicking an in-page link is filed as internal, never counted as Direct traffic.
curl -X POST https://api.vectraseo.com/api/v1/projects/$PROJECT_ID/traffic/daily \
-H "x-api-key: vsak_..." -H "content-type: application/json" \
-d '{
"date": "2026-10-02",
"source": "cloudfront",
"rows": [
{ "path": "/", "referrer_host": "chatgpt.com", "country": "US",
"status_class": "2xx", "agent_class": "human", "agent_name": "Chrome",
"requests": 41, "edge_hits": 39 },
{ "path": "/blog/post", "self_referrer": true, "status_class": "2xx",
"agent_class": "human", "requests": 12, "edge_hits": 10 },
{ "path": "/llms.txt", "status_class": "2xx", "agent_class": "ai_crawler",
"agent_name": "GPTBot", "requests": 6, "edge_hits": 6 }
]
}'- A replay replaces the day. Ingest is idempotent per project, source and date, so retries never inflate counts.
- Only a 2xx response is a visit. A human row with
status_class3xx, 4xx or 5xx is stored, but never counted as a visit: a redirect hop and the page it leads to are one visit, and a miss or an error is not a page a reader saw. Keep sending those rows; they feed the status breakdown. - A visit is a request, not a person. Visit totals sum
requestsover 2xx human rows, so two page loads by one reader are two visits, and the feed carries no visitor identifier to dedupe on. Compare them with Google Analytics sessions or users only as a rough order of magnitude. - Send
humanonly for clients that rendered your site. VectraSEO trusts youragent_class, and a browser user agent alone is not enough: headless clients and scanners send one too. A reliable signal is that the client also fetched your own JS or CSS bundle. Anything that looks like a browser but never loaded one belongs inother_bot, which never counts as a visit. - Drop probe paths before sending. Scanner requests for paths such as
/.env,/.git/or/wp-adminare not pages. Remove them, or fold them into a path such as/other, so they cannot get a day's body rejected. - Internal navigation is not a visit. Set
self_referrer: truewhen the request's referrer was your own site, and omitreferrer_host. It is filed asinternaland never counted as an arrival, so a reader clicking an in-page link is not read as Direct traffic. - Each day keeps its 1,000 busiest pages (the rest are reported as
pages_truncated); referrer, UTM, country, crawler and status breakdowns keep their top 50 and fold the rest intoother. - A visit counts as an AI referral when its referrer host or
utm_sourceis an assistant such as chatgpt.com, perplexity.ai, claude.ai or gemini.google.com. - Send aggregates only. No IP addresses, raw user agents, cookies or full query strings; fold referrer or country buckets under 3 requests into
other.
Responses: 200 {accepted, date, source, pages, pages_truncated, unmatched_rows}. 403 when the key lacks traffic:write, is restricted to other projects, or the plan is not paid; these carry a JSON detail. A 403 without one came from our network edge, not the API: check the rows for probe paths, remove them and send the day again (a replay replaces it). 413 when the body is over 4 MiB: aggregate further. 422 for a schema violation or a project with no website URL; do not retry. 429: retry after Retry-After. 5xx: retry with backoff.
Errors and limits
401 for an unknown, malformed or revoked key (the cases are not distinguished, so a caller cannot probe which applied). 403 names the missing scope, or carries code: "subscription_limit" when the plan is the blocker. 404 means the resource does not exist or is not yours. Quota is per account, in a fixed hourly window shared with the Analytics and Agent APIs, and every response carries X-RateLimit-*.
Can't find what
you're looking for?
Reach out to the team. We answer every email and we read every bug report.