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.

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 data appears on the project card as "Visits (site logs)" when GA4 has no data, in GET /traffic/overview and GET /traffic/pages, and as context on each recommendation.

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": "/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.
requests, edge_hits
Integers, 0 or more. Rows with requests: 0 are ignored.
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.
  • 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 or the plan is not paid. 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.