skip to content →
FIELD_MANUAL / LIVEREVISIONv1.3 · 2026.10.04Updated 2026-10-04CHAPTERS12 FILEDCH_05INTEGRATION
[ CH_05.02 / FIELD_MANUAL ]

Provisioning API

[ CH_05.02 / 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_.

> confirm_your_key
curl -H "x-api-key: vsak_..." https://api.vectraseo.com/api/v1/account/me

Onboard 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.

onboard.sh
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.

SCOPE
GRANTS
projects:read
Read projects, posts, gaps, monitors, scans and jobs.
recommendations:read
Open a Search Console recommendation's detail, and act on one. Required by every /recommendations route — you cannot apply a fix you are not allowed to read.
projects:write
Create projects and edit project settings and configuration. Also removes recoverable configuration: a competitor, an AI-visibility monitor or a tracked rank keyword; DELETE /schedule only switches the schedule off.
jobs:write
Start content runs, site scans and auto-fixes. Consumes plan quota and model budget.
content:publish
Publish and republish posts to the connected CMS. Writes to your live website.
traffic:write
Push your own aggregated traffic (per page, per UTC day) into a project, e.g. a nightly CDN log rollup. A replay replaces that day.
gaps:delete
Permanently delete a suggested topic. The only scope that exists just to delete — prefer rejecting one (projects:write), which is kept as a negative example.

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:write removes a competitor, an AI-visibility monitor or a tracked rank keyword (each can be added back) and can switch the content schedule off, and gaps:delete removes a suggested topic, which POST /gaps/identify rebuilds.
  • 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.

push-traffic.sh
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 }
    ]
  }'
FIELD
RULES
date
A UTC day (YYYY-MM-DD), at most 400 days old and not in the future.
source
cloudfront for now.
rows
At most 10,000 rows; the body may be up to 4 MiB.
path
The URI stem only, starting with /. A ? or # is rejected with 422. /a and /a/ count as one page.
agent_class
human, search_crawler, ai_crawler or other_bot.
status_class
2xx, 3xx, 4xx or 5xx. Only 2xx human rows count as visits.
requests, edge_hits
Integers, 0 or more. Rows with requests: 0 are ignored.
self_referrer
true when the request came from your own pages (an in-page navigation). Send it with referrer_host omitted: it is filed as internal and never counted as a visit. Defaults false.
referrer_host, country, agent_name, utm_*
Optional breakdowns. Unknown fields are ignored and never stored.
  • 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_class 3xx, 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 requests over 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 human only for clients that rendered your site. VectraSEO trusts your agent_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 in other_bot, which never counts as a visit.
  • Drop probe paths before sending. Scanner requests for paths such as /.env, /.git/ or /wp-admin are 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: true when the request's referrer was your own site, and omit referrer_host. It is filed as internal and 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 into other.
  • A visit counts as an AI referral when its referrer host or utm_source is 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-*.

[ END_OF_MANUAL ]
[ NEED_HELP? ]

Can't find what
you're looking for?

Reach out to the team. We answer every email and we read every bug report.