Docs / Reference
REST API
Authentication, the response envelope, every route, and how dry runs, refreshes and ceilings work.
The dashboard is built on this API, so anything you can do in the app you can script. The base URL is {API_URL}/api (http://localhost:4100/api on a default self-hosted install).
Authentication
Send an OpenSEO API key as Authorization: Bearer oseo_…. Create keys under Settings; a key is shown once and acts as you, so treat it like a password. Keys can be revoked at any time and cannot create further keys. The dashboard itself uses a session cookie and may only make changes from its own origin.
Responses
Every response is JSON: { "ok": true, "data": … } on success, or { "ok": false, "error": { "code", "message", "details?" } } with a 4xx or 5xx status. Codes are listed in Errors and troubleshooting. IDs are UUIDs; a malformed one is a 404.
Lookups
POST /projects/:projectId/lookups/:name runs one paid lookup with its input as the JSON body. Inputs are validated before anything is spent; omitted market fields default to the project's.
| Parameter | Effect |
|---|---|
| ?dryRun=1 | Return { estimate: { min, max } } and spend nothing. max is null for token-priced lookups. |
| ?refresh=1 | Skip the cache and pay for fresh data. |
| ?maxCostUsd=0.50 | Refuse with cost_ceiling_exceeded if the worst-case price is higher. Must be a positive number. |
# Price a lookup (free), then run it with a ceiling
curl -X POST "$API_URL/api/projects/$PROJECT/lookups/keyword_ideas?dryRun=1" \
-H "Authorization: Bearer $OSEO_KEY" -H "Content-Type: application/json" \
-d '{"keyword":"emergency plumber","limit":150}'
curl -X POST "$API_URL/api/projects/$PROJECT/lookups/keyword_ideas?maxCostUsd=0.10" \
-H "Authorization: Bearer $OSEO_KEY" -H "Content-Type: application/json" \
-d '{"keyword":"emergency plumber","limit":150}'{
"ok": true,
"data": {
"name": "keyword_ideas",
"estimate": { "min": 0.03, "max": 0.08 },
"data": { "seed": "emergency plumber", "keywords": [ ... ] },
"costUsd": 0.08,
"cached": false,
"fetchedAt": "2026-10-15T09:30:00.000Z"
}
}cached: true results have costUsd: 0. If some parts of a multi-part lookup (the domain overview) fail, the parts that were paid for are returned with an errors list and are not cached. Names: keyword_ideas, keyword_metrics, serp, autocomplete, keyword_trends, domain_overview, domain_keywords, domain_pages, competitors_domain, competitors_serp, keyword_gap, backlinks_summary, backlinks_history, backlinks_list, backlinks_referring_domains, backlinks_anchors, backlinks_pages, link_intersect, onpage_check, lighthouse, maps_search, maps_place, maps_reviews, ai_compare. GET /lookups lists them with descriptions and cache times.
Account
| Route | Notes |
|---|---|
| GET /me | User and settings: whether a YepAPI key is set, its hint, the monthly limit. |
| PUT /settings/yepapi-key | { apiKey }. Verified without spending credits. Never returned. |
| DELETE /settings/yepapi-key | Remove the key. |
| PUT /settings/budget | { monthlyBudgetUsd: number | null }. |
| GET /balance | Live YepAPI balance. Free, and works when the monthly limit is reached. |
| GET /usage | This month's spend in total, by feature and by day, and the last 50 calls. |
| GET, POST /api-keys; DELETE /api-keys/:id | List, create ({ name }; the key is in the reply once) and revoke. Creating needs a signed-in session. |
| GET /locations?q=&country= | Location codes. Free. |
| GET /ai/models | The chat model catalog and the default set. |
Projects
GET/POST /projects, GET/PATCH/DELETE /projects/:projectId. Fields: name, domain, locationCode, locationName, country, language, context (free-form notes for agents). When locationCode comes without country, the country (and a missing name) is looked up from the code; an unknown code is refused rather than guessed. Deleting a project deletes everything in it.
Saved keywords
| Route | Notes |
|---|---|
| GET /projects/:id/saved-keywords | ?q= &tag= &sort=keyword|volume|cpc|difficulty|createdAt &order= &limit= (max 500) &offset=. Returns { total, keywords, tags }. |
| POST /projects/:id/saved-keywords | { keywords: [{ keyword, volume?, cpc?, competition?, difficulty?, intent?, trend? }], tags?, locationCode?, language? }. Up to 1,000; saving again updates figures and merges tags. |
| PATCH /projects/:id/saved-keywords/tags | { ids, add?, remove? }. |
| POST /projects/:id/saved-keywords/delete | { ids }. |
| POST /projects/:id/saved-keywords/refresh | Paid. Refreshes keywords in the project’s market and language. ?dryRun=1 returns { estimate: { keywords, otherMarkets, min, max } }. |
Rank tracking
| Route | Notes |
|---|---|
| GET, POST /projects/:id/trackers | Create with { domain, locationCode or latitude + longitude, locationName, language?, devices?, depth? (10–100, by 10), schedule?, keywords? }. |
| GET, PATCH, DELETE /trackers/:trackerId | PATCH { name?, devices?, depth?, schedule? }. GET includes the price range of one check and the last 20 runs. |
| POST /trackers/:trackerId/keywords | { keywords }. Up to 1,000 per tracker. |
| POST /trackers/:trackerId/keywords/delete | { ids }. Removed keywords stop being checked; their history is kept, and adding one again restores it. |
| PATCH /trackers/:trackerId/keywords/:keywordId | { pinned?, tags? }. |
| POST /trackers/:trackerId/runs | Paid. 202 with the queued run; 409 if one is already going. |
| GET /rank-runs/:runId; POST /rank-runs/:runId/cancel | Poll progress and cost; cancel. |
| GET /trackers/:trackerId/results?device= | Latest positions, changes, history and a summary. |
| GET /trackers/:trackerId/competitors?device= | Domains most often in the top 10. |
| GET /trackers/:trackerId/keywords/:keywordId/history?device= | Every check for one keyword and the competing results of the latest. |
AI visibility
| Route | Notes |
|---|---|
| GET /projects/:id/ai | Settings, prompts, the active run and the estimate { answers, min, expected }. |
| PUT /projects/:id/ai/settings | { brand?, brandAliases?, domain?, competitors?, models?, webSearch?, schedule? }. |
| POST /projects/:id/ai/prompts; POST …/prompts/delete | { prompts, topic? } and { ids }. Up to 200 per project. |
| PATCH /projects/:id/ai/prompts/:promptId | { active?, topic? }. |
| POST /projects/:id/ai/suggest-prompts | Paid (tokens). { description, count? }. |
| POST /projects/:id/ai/runs | Paid. { maxCostUsd? }: refused if the likely cost is higher, and stopped early (status cancelled) before an answer that would likely cross it. |
| GET /projects/:id/ai/runs/:runId; POST …/cancel | Poll and cancel. |
| GET /projects/:id/ai/results | The latest run in detail, the trend, and comparison: { status: comparable | incomplete | scope_changed | no_previous | no_data, mentionChange, citeChange, cells }. Prompts that name the brand and failed or empty answers are left out of rates. |
| GET /projects/:id/ai/prompts/:promptId/answers | Every stored answer for a prompt. |
Site audit
| Route | Notes |
|---|---|
| GET, POST /projects/:id/audits | POST { startUrl?, maxPages? (≤ 5,000), renderJs? (≤ 500 pages) }; ?dryRun=1 for the price. A plain crawl is free. |
| GET, DELETE /audits/:auditId; POST /audits/:auditId/cancel | GET includes the summary and issue groups with explanations. |
| GET /audits/:auditId/issues | ?type= &severity= &limit= (≤ 1,000) &offset=. |
| GET /audits/:auditId/pages | ?q= &sort=issues|url|status|words|response|depth &limit= &offset=. |
| GET /audits/:auditId/pages/:pageId | Everything recorded for one page, and its issues. |
| GET /audit/issue-catalog | Every issue type with severity, explanation and fix. |
Local SEO
| Route | Notes |
|---|---|
| GET, POST /projects/:id/geo-grids | POST { keyword, placeId, placeName, centerLat, centerLng, size (3|5|7|9), spacingM (100–20,000) }; ?dryRun=1 for the price ($0.07 a point). |
| GET, DELETE /geo-grids/:gridId; POST /geo-grids/:gridId/cancel | GET includes every point. Points searched before a cancel stay billed. |
Search Console and Analytics
Free: these read the user's own Google data and spend nothing from YepAPI. Connecting is done in the dashboard (GET /google/:provider/connect starts Google's consent screen and needs a signed-in session, not an API key). Search Console periods end three days ago; GA4 periods end yesterday.
| Route | Notes |
|---|---|
| GET /projects/:id/google | { configured, redirectUris, gsc, ga4, accounts }. |
| GET /google/accounts; DELETE /google/accounts/:accountId | Your Google grants. Deleting one disconnects every project using it and returns { projects }. |
| GET /projects/:id/google/gsc/sites?accountId= | Search Console properties, with verified: false for ones you cannot read. |
| PUT /projects/:id/google/gsc | { accountId, siteUrl }. Unverified properties are refused. |
| GET /projects/:id/google/ga4/properties?accountId= | GA4 properties in the account. |
| PUT /projects/:id/google/ga4 | { accountId, propertyId: "properties/123" }. |
| DELETE /projects/:id/google/:provider | Disconnect gsc or ga4 from the project. |
| GET /projects/:id/search-performance | ?range=last_7_days|last_28_days|last_3_months &device= &country= (alpha-3) &pageText= &pageMatch=contains|equals &queryText= &queryMatch=. Totals against the previous period, daily rows, striking distance, countries. |
| GET /projects/:id/search-performance/table | The same filters plus ?dimension=query|page &page= &size=25|50|100. Returns { rows, hasMore }. |
| GET /projects/:id/search-console/rows | ?dimensions=query,page (up to 3) &minPosition= &maxPosition= &minImpressions= &limit= (≤ 1,000) and the filters above. |
| POST /projects/:id/search-console/inspect | { urls } (1 to 10). Index status per URL. |
| GET /projects/:id/analytics/overview | ?range= &channel=organic|all. Sessions, engagement, key events and revenue against the previous period, plus a daily series. |
| GET /projects/:id/analytics/landing-pages | ?range= &channel= &limit= (≤ 1,000). |
| GET /projects/:id/search-opportunities | ?range=. Pages at positions 4 to 20 with a 0 to 100 score and its parts. |
Free tools (public)
GET /public/tools lists the tools and markets. POST /public/tools/:tool runs one, with no account, behind the guards described in Run the free tools. These routes are meant for the site, not for scripts: use an API key and the lookups above instead.