Skip to content

API Documentation

Programmatic access to keyword rankings, SERP data, keyword research, competitor analysis, and Core Web Vitals.

Base URLhttps://serpdino.com
AuthAuthorization: Bearer sd_your_key_here

Create and manage your API keys in Dashboard → Settings → API Keys. Maximum 5 active keys per user.

Use with AI agents (MCP)

Point any MCP-compatible client at this URL with your API key as the bearer token. All 28 tools become available immediately — no install required.

https://serpdino.com/api/mcp

Works with Claude (web & desktop), ChatGPT (Developer mode), Cursor, Claude Code, and any client speaking the Model Context Protocol. See the serpdino-mcp repo for local-install setup.

Quick Start

1. List your projects

cURL
curl -H "Authorization: Bearer sd_your_key_here" \
  https://serpdino.com/api/projects

2. Add keywords to a project

cURL
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID","keywords":["seo tools","rank tracker"],"geoCode":"US","langCode":"en"}' \
  https://serpdino.com/api/projects/keywords

3. Get keyword rankings

cURL
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/keyword-updates?projectId=PROJECT_ID"

Errors

All error responses are JSON. The error message lives in the `message` field (NOT `error`).

Some responses include `success: false` alongside `message`; account/auth errors may omit `success` entirely. Clients should treat any non-2xx status OR `success === false` as an error.

`/api/tools/keyword-research` returns HTTP 200 with `{ success: false, message }` for all expected failures (bad seed, unsupported geo/lang, upstream timeout). Always check `success` in addition to HTTP status.

`/api/projects/keyword-ideas` returns HTTP 200 with `{ success: true, ideas: [] }` even when the upstream scraper is unreachable — an empty `ideas` array means "no data available right now," not necessarily success.

Write endpoints that mutate project data (`POST/DELETE` on `/api/projects/keywords`, `/api/scrape/new-keywords`, `POST/DELETE` on `/api/projects/notes`, `/api/tools/keyword-research`, `/api/projects/keyword-ideas`, `/api/projects/keyword-suggestions`, folder writes) require an **active subscription** — return HTTP 403 `{ message: "Account suspended", suspended: true }` if the account is suspended.

Many read endpoints bypass auth entirely when the target project has `shared: true` (public dashboard links). Affected: `/api/projects/:id`, `keyword-updates`, `position-history`, `keyword-volumes`, `competitor-positions`, `pages`, `pages-pagespeed`, `notes` (GET), `export`, `export-agent`.

These domain-level read endpoints are fully **public** (no API key required): `/api/projects/competitors-filtered`, `/api/projects/pagespeed`, `/api/projects/crux-history`, `/api/projects/similarweb`. They read from our DB cache only — no auth needed. Each is rate-limited to 60 req / min (600 / hour) per IP.

Typical error body
{ "success": false, "message": "Description of what went wrong" }
400Bad RequestMissing required parameter or invalid value
401UnauthorizedMissing or invalid API key. Body: { message } or { success: false }
403ForbiddenAccount suspended. Body: { message: "Account suspended", suspended: true }
404Not FoundResource (project, folder, note) does not exist or belongs to another user
405Method Not AllowedHTTP method not supported for this endpoint
429Too Many RequestsRate limit exceeded. /api/tools/serp-check and /api/tools/traffic-check allow 5 req / 60s per API key. /api/tools/keyword-research and /api/projects/keyword-ideas allow 10 req / min (100 / hour) per account. Public endpoints (competitors-filtered, pagespeed, crux-history, similarweb) allow 60 req / min (600 / hour) per IP. Body includes retryAfter (seconds); a Retry-After header is also set.
500Server ErrorInternal server error

Projects

Create and manage SEO tracking projects.

GET/api/projects

List all projects

Response 200
{
  "success": true,
  "projects": [
    {
      "_id": "65f1c2a8e1234567890abcde",
      "name": "Acme Corp",
      "domain": "acme.com",
      "aliases": [],
      "competitors": [
        "competitor.com"
      ],
      "folder": null,
      "updateFrequency": "daily",
      "serpTop": 50,
      "createdDate": "2025-01-15T08:32:11.000Z"
    }
  ]
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  https://serpdino.com/api/projects
GET/api/projects/:id

Get project details

Includes keywords[], domain, competitors[], and full settings.

Parameters
idstringpathrequiredProject ID (Mongo ObjectId)
Response 200
{
  "success": true,
  "project": {
    "_id": "65f1c2a8e1234567890abcde",
    "name": "Acme Corp",
    "domain": "acme.com",
    "competitors": [
      "competitor.com"
    ],
    "keywords": [
      {
        "_id": "65f1c2a8e1234567890fffff",
        "keyword": "seo tools",
        "geoCode": "US",
        "langCode": "en"
      }
    ]
  }
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  https://serpdino.com/api/projects/PROJECT_ID
POST/api/projects201

Create a new project

Returns HTTP 201 on success. Fires fire-and-forget triggers for keyword suggestions, SimilarWeb, PageSpeed, and competitor analysis.

Parameters
namestringbodyrequiredProject name
domainstringbodyrequiredTarget domain (e.g. example.com). Lowercased server-side.
folderstringbodyFolder ID to place project in
iconstringbodyIcon identifier or URL
aliasesstringbodyComma/newline-separated alternative domains (treated as same property)
competitorsstring[]bodyCompetitor domains (PUT only — POST ignores this field)
sharedbooleanbodyWhether project is shared via public link (default: false)
updateFrequencyenumbodydaily | every3days | weekly | biweekly | custom (default: daily)
customUpdateDaysnumber[]bodyRequired when updateFrequency=custom. Array of weekday numbers 0–6 (0=Sunday).
updateTimenumberbodyHour of day 0–23 to run updates (default: 12)
updateTimezonestringbodyIANA timezone name (default: UTC)
serpTopnumberbody50 | 100 — depth of SERP scrape (default: 50)
Response 201
{
  "success": true,
  "project": {
    "_id": "65f1c2a8e1234567890abcde",
    "name": "My Project",
    "domain": "example.com",
    "updateFrequency": "daily",
    "serpTop": 50
  },
  "message": "Project created successfully"
}
Example (cURL)
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name":"My Project","domain":"example.com"}' \
  https://serpdino.com/api/projects
PUT/api/projects

Update project settings

_id, name, and domain are ALL required (the handler rejects with 400 if any is missing). To change a single field, send the existing values for the others.

Parameters
_idstringbodyrequiredProject ID
namestringbodyrequiredProject name (required even if unchanged)
domainstringbodyrequiredTarget domain (required even if unchanged)
competitorsstring[]bodyArray of competitor domains
folderstringbodyFolder ID, or empty string to remove from folder
iconstringbodyIcon identifier or URL
aliasesstringbodyComma/newline-separated alias domains
sharedbooleanbodyPublic sharing toggle
updateFrequencyenumbodydaily | every3days | weekly | biweekly | custom
customUpdateDaysnumber[]bodyRequired when updateFrequency=custom. Weekday numbers 0–6.
updateTimenumberbodyHour 0–23
updateTimezonestringbodyIANA timezone
serpTopnumberbody50 | 100
Response 200
{
  "success": true,
  "project": {
    "_id": "65f1c2a8e1234567890abcde",
    "name": "Acme",
    "domain": "acme.com",
    "competitors": [
      "competitor.com"
    ]
  },
  "message": "Project updated successfully"
}
Example (cURL)
curl -X PUT \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"_id":"PROJECT_ID","name":"Acme","domain":"acme.com","competitors":["competitor.com"]}' \
  https://serpdino.com/api/projects
DELETE/api/projects

Delete a project and all its data

Parameters
_idstringbodyrequiredProject ID
Response 200
{
  "success": true,
  "message": "Project deleted successfully",
  "projects": [],
  "folders": []
}
Example (cURL)
curl -X DELETE \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"_id":"PROJECT_ID"}' \
  https://serpdino.com/api/projects

Folders

Organise projects into folders.

GET/api/projects/folders

List folders

Response 200
{
  "success": true,
  "folders": [
    {
      "_id": "FOLDER_ID",
      "name": "Clients",
      "position": 0
    }
  ]
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  https://serpdino.com/api/projects/folders
POST/api/projects/folders201

Create folder

Returns HTTP 409 `{ message: "Folder with this name already exists" }` if a folder with that name (case-insensitive) already exists for this user. Requires active subscription.

Parameters
namestringbodyrequiredFolder name
Response 201
{
  "success": true,
  "folder": {
    "_id": "FOLDER_ID",
    "name": "My Folder",
    "position": 0
  },
  "message": "Folder created successfully"
}
Example (cURL)
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name":"My Folder"}' \
  https://serpdino.com/api/projects/folders
PUT/api/projects/folders

Rename folder

Returns HTTP 409 on name conflict. Requires active subscription.

Parameters
idstringqueryrequiredFolder ID
namestringbodyrequiredNew folder name
Response 200
{
  "success": true,
  "folder": {
    "_id": "FOLDER_ID",
    "name": "New Name"
  },
  "message": "Folder updated successfully"
}
Example (cURL)
curl -X PUT \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name":"New Name"}' \
  "https://serpdino.com/api/projects/folders?id=FOLDER_ID"
DELETE/api/projects/folders

Delete folder

Detaches all projects in the folder (they remain, just unassigned), then deletes the folder. Returns the refreshed folders[] and projects[] lists. Requires active subscription.

Parameters
idstringqueryrequiredFolder ID
Response 200
{
  "success": true,
  "message": "Folder \"My Folder\" deleted successfully",
  "folders": [],
  "projects": []
}
Example (cURL)
curl -X DELETE -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/folders?id=FOLDER_ID"

Keywords

Track keywords inside a project.

POST/api/projects/keywords

Add keywords to track in a project

Max 3500 keywords per project. Keywords appear in rankings after the next scheduled SERP check. Requires active subscription. Duplicates within the request and against existing project keywords are silently deduped. When the limit is exceeded, returns HTTP 400 with extended fields `{ error: "KEYWORDS_LIMIT_EXCEEDED", currentCount, maxAllowed, availableSlots }`.

Parameters
projectIdstringbodyrequiredProject ID
keywordsstring[]bodyrequiredArray of keyword strings (lowercased server-side)
geoCodestringbodyrequiredISO-2 country code (e.g. US). Required — no default.
langCodestringbodyrequiredISO-2 language code (e.g. en). Required — no default.
Response 200
{
  "success": true,
  "message": "Added 2 new keywords",
  "addedCount": 2,
  "project": {
    "_id": "PROJECT_ID",
    "keywords": [
      "KW_ID_1",
      "KW_ID_2"
    ]
  }
}
Example (cURL)
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID","keywords":["seo tools","rank tracker"],"geoCode":"US","langCode":"en"}' \
  https://serpdino.com/api/projects/keywords
DELETE/api/projects/keywords

Remove tracked keywords

Parameters
projectIdstringbodyrequiredProject ID
keywordIdsstring[]bodyrequiredArray of keyword IDs to remove
Response 200
{
  "success": true,
  "removedCount": 2,
  "project": {
    "_id": "PROJECT_ID"
  }
}
Example (cURL)
curl -X DELETE \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID","keywordIds":["KW_ID_1","KW_ID_2"]}' \
  https://serpdino.com/api/projects/keywords
POST/api/scrape/new-keywords

Trigger a fresh SERP check for keywords

Async — returns immediately (HTTP 200, status: "processing") while scraping happens in the background. Costs 1 balance credit per keyword (×2 for serpTop=100); rejects with HTTP 400 "Insufficient balance" if `keywordIds.length > virtualBalance`. Pass all project keyword IDs to refresh everything. Requires active subscription. All `keywordIds` must belong to the given project.

Parameters
projectIdstringbodyrequiredProject ID
keywordIdsstring[]bodyrequiredKeyword IDs to refresh (must belong to projectId)
Response 200
{
  "success": true,
  "message": "Accepted 10 keywords for processing. Position updates started in background mode.",
  "status": "processing",
  "summary": {
    "total": 10,
    "accepted": 10,
    "errors": 0
  }
}
Example (cURL)
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID","keywordIds":["KW_ID_1"]}' \
  https://serpdino.com/api/scrape/new-keywords

Ranking Data

Read keyword positions, SERP snapshots, and volume data.

GET/api/projects/keyword-updates

Get keyword position history

Public when project is shared. Response.data is keyed by keyword ID; each entry contains the full Keywords document plus an `updates` array of dated check results. When startDate/endDate are omitted, defaults to last 30 days.

Parameters
projectIdstringqueryrequiredProject ID
startDatestringqueryYYYY-MM-DD (default: 30 days ago)
endDatestringqueryYYYY-MM-DD (default: today)
aggregationenumquerydaily | weekly | monthly (default: daily)
searchstringqueryFilter keywords by substring (case-insensitive)
geoCodestringqueryFilter by country code (uppercased)
langCodestringqueryFilter by language code (lowercased)
sortByenumquerykeyword | position | volume
sortOrderenumqueryasc | desc (default: desc)
Response 200
{
  "success": true,
  "data": {
    "65f1c2a8e1234567890fffff": {
      "keyword": {
        "_id": "65f1c2a8e1234567890fffff",
        "value": "seo tools",
        "geoCode": "US",
        "langCode": "en",
        "avgMonthlySearches": 8100
      },
      "updates": [
        {
          "_id": "UPD_ID",
          "date": "2025-01-16T12:00:00.000Z",
          "status": "success",
          "position": {
            "position": 9,
            "url": "https://example.com/seo-tools",
            "title": "...",
            "domain": "example.com"
          },
          "aiSerpId": null
        },
        {
          "_id": "UPD_ID2",
          "date": "2025-01-15T12:00:00.000Z",
          "status": "success",
          "position": {
            "position": 12,
            "url": "https://example.com/seo-tools",
            "title": "...",
            "domain": "example.com"
          },
          "aiSerpId": null
        }
      ]
    }
  }
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/keyword-updates?projectId=PROJECT_ID&startDate=2025-01-01&endDate=2025-01-31"
GET/api/projects/position-history

Get full SERP snapshot for a keyword check

Returns top 30 results plus the project domain itself if it ranks beyond 30. Each result includes movement vs the previous successful check (`up`/`down`/`same`/`new`). Public when project is shared.

Parameters
keywordUpdateIdstringqueryrequiredKeyword update ID (from keyword-updates response)
Response 200
{
  "success": true,
  "data": {
    "keywordId": "65f1c2a8e1234567890fffff",
    "keywordValue": "seo tools",
    "date": "2025-01-16T12:00:00.000Z",
    "resultList": [
      {
        "position": 1,
        "title": "...",
        "domain": "example.com",
        "url": "https://example.com",
        "previousPosition": 2,
        "movement": {
          "type": "up",
          "value": 1
        }
      }
    ],
    "droppedDomains": [
      {
        "domain": "old-competitor.com",
        "previousPosition": 15,
        "movement": {
          "type": "gone",
          "value": null
        }
      }
    ],
    "hasPreviousData": true,
    "aiOverviewResults": [
      {
        "position": 1,
        "title": "...",
        "domain": "wikipedia.org",
        "url": "https://...",
        "isOurDomain": false
      }
    ],
    "aiContainsOurDomain": false
  }
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/position-history?keywordUpdateId=UPDATE_ID"
GET/api/projects/keyword-volumes

Get search volume, CPC, and competition data

Two modes with different response shapes. Bulk (no keywordId) uses compact field names to minimise payload size. Single mode (with keywordId) returns the full per-keyword detail. CPC values are pre-converted to the project currency. Public when project is shared.

Parameters
projectIdstringqueryrequiredProject ID
keywordIdstringquerySingle keyword ID — when present, returns full volume history for one keyword instead of the bulk map
Response 200
{
  "success": true,
  "data": {
    "65f1c2a8e1234567890fffff": {
      "v": "seo tools",
      "g": "US",
      "l": "en",
      "s": "google",
      "lv": 8100,
      "tr": [
        6600,
        7200,
        7800,
        8000,
        8100,
        8100,
        7900,
        8000,
        8100,
        8200,
        8000,
        8100
      ],
      "am": 8100,
      "ci": 72,
      "cl": 3.1,
      "ch": 5.4
    }
  }
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/keyword-volumes?projectId=PROJECT_ID"

Keyword Research

Discover new keywords and run live SERP / traffic checks.

POST/api/tools/keyword-research

Research keyword ideas with volumes, CPC, competition, and trends

Requires active subscription. Rate limited for API-key callers: 10 req / min, 100 req / hour per account — HTTP 429 with `retryAfter` (seconds) when exceeded. All expected failures (bad seed, unsupported geo/lang, upstream timeout, scraper error) are returned as HTTP 200 with `{ success: false, message, error? }` — always check `success`. 60s upstream timeout. `pageSize` is clamped 10–500, `trendsForTopN` is clamped 0–200.

Parameters
seedstringbodyrequiredSeed keyword, URL, or domain (mode-dependent)
modeenumbodykeyword | url | domain (default: keyword)
geostringbodyISO-2 country code (default: US)
langstringbodyISO-2 language code (default: en)
pageSizenumberbody10–500 (default: 100)
trendsForTopNnumberbody0–200 (default: 60) — number of top ideas to enrich with monthly trend data
enrichWithLlmbooleanbodyApply LLM intent/cluster tagging (default: true)
Response 200
{
  "success": true,
  "ideas": [
    {
      "keyword": "best seo tools",
      "avgMonthlySearches": 8100,
      "competition": "HIGH",
      "cpc": 4.21,
      "intent": "commercial",
      "cluster": "tools"
    }
  ],
  "meta": {
    "totalResults": 100,
    "geo": "US",
    "lang": "en"
  }
}
Example (cURL)
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"seed":"best seo tools","geo":"US","lang":"en","pageSize":50}' \
  https://serpdino.com/api/tools/keyword-research
POST/api/tools/serp-check

Live SERP check — returns top results and search volume for a keyword

Rate limited: 5 req / 60s. When called with an API key (`Authorization: Bearer sd_...`), the limit is **per API user**. When called from the web (Turnstile token), the limit is per client IP. 429 response body includes `retryAfter` (seconds), `limit`, `window`. Returns HTTP 502 if both upstream SERP and volume calls fail.

Parameters
keywordstringbodyrequiredSearch query
geostringbodyrequiredISO-2 country code (e.g. US)
langstringbodyrequiredISO-2 language code (e.g. en)
Response 200
{
  "success": true,
  "data": {
    "serp": [
      {
        "position": 1,
        "url": "https://...",
        "title": "...",
        "snippet": "..."
      }
    ],
    "volume": {
      "value": 1300,
      "cpc": 2.5,
      "competition": "MEDIUM"
    }
  },
  "remaining": 4
}
Example (cURL)
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"keyword":"rank tracker","geo":"US","lang":"en"}' \
  https://serpdino.com/api/tools/serp-check
POST/api/tools/traffic-check

Domain traffic check — returns SimilarWeb stats and PageSpeed data

Rate limited: 5 req / 60s — per API user when called with an API key, per IP when called from the web. Triggers a live scraper fetch on DB cache miss for either SimilarWeb or PageSpeed (slow path), or returns cached data immediately. `data` may be null if SimilarWeb has no info on the domain. Domain is validated and normalised server-side (strips protocol, `www.`, path).

Parameters
domainstringbodyrequiredTarget domain (e.g. example.com or https://www.example.com/path — normalised server-side)
Response 200
{
  "success": true,
  "domain": "example.com",
  "data": {
    "stats": {
      "domain": "example.com",
      "totalVisits": 1234567,
      "engagementMetrics": {
        "bounceRate": 0.42,
        "avgVisitDuration": 134
      },
      "fetchedAt": "2025-01-16T08:00:00.000Z"
    },
    "monthlyVisits": [
      {
        "domain": "example.com",
        "month": "2025-01-01",
        "visits": 1234567
      }
    ]
  },
  "pagespeed": {
    "domain": "example.com",
    "performanceScore": 86,
    "cruxScore": 78,
    "largestContentfulPaint": 1.8,
    "cumulativeLayoutShift": 0.05,
    "fetchedAt": "2025-01-16T08:00:00.000Z"
  },
  "remaining": 4
}
Example (cURL)
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"domain":"example.com"}' \
  https://serpdino.com/api/tools/traffic-check
GET/api/projects/keyword-suggestions

AI-generated keyword suggestions for a project

Polling endpoint — `ready: false` (with no other fields) means the LLM pipeline is still running. Retry every few seconds for ~2 minutes max. Generation is auto-triggered at project creation, so this is just a reader over the cached suggestions doc. Requires active subscription. Each locale ships up to 30 phrases, sorted by lastVolume desc.

Parameters
projectIdstringqueryrequiredProject ID
Response 200
{
  "success": true,
  "ready": true,
  "domain": "example.com",
  "primary": {
    "lang": "en",
    "geo": "US"
  },
  "locales": [
    {
      "key": "en-US",
      "lang": "en",
      "region": "US",
      "geo": "US",
      "lastUpdatedAt": "2025-01-16T08:00:00.000Z",
      "keywords": [
        {
          "phrase": "best seo tools",
          "lastVolume": 8100,
          "volume": [
            {
              "year": 2024,
              "months": [
                {
                  "month": 12,
                  "value": 8100
                }
              ]
            }
          ]
        }
      ]
    }
  ],
  "pages": [],
  "meta": {
    "seedUsed": "domain"
  },
  "runCount": 1,
  "lastRunAt": "2025-01-16T08:00:00.000Z",
  "lastError": null
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/keyword-suggestions?projectId=PROJECT_ID"
POST/api/projects/keyword-ideas

Keyword ideas based on a project's domain

Soft-fail endpoint: always returns HTTP 200 with `success: true` when authorised. On upstream errors returns `{ success: true, ideas: [], error: "upstream_error" | "service_unavailable" }` — an empty `ideas` array does NOT mean failure, it means "no data available right now". 30s timeout on the upstream call. Requires active subscription. Rate limited for API-key callers: 10 req / min, 100 req / hour per account — HTTP 429 with `retryAfter` when exceeded.

Parameters
projectIdstringbodyrequiredProject ID
geostringbodyCountry code (default: US, uppercased)
langstringbodyLanguage code (default: en, lowercased)
Response 200
{
  "success": true,
  "ideas": [
    {
      "keyword": "seo tools comparison",
      "avgMonthlySearches": 1000,
      "competition": "MEDIUM"
    }
  ]
}
Example (cURL)
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID"}' \
  https://serpdino.com/api/projects/keyword-ideas

Competitors

Compare competitor rankings and traffic.

GET/api/projects/competitor-positions

Get a competitor's ranking positions across all tracked keywords

Searches the project's tracked keywords' SERP history for results matching the competitor domain. `position: 0` means the competitor didn't appear in the top 30 results for that check. Public when project is shared. Deduped to one entry per calendar day per keyword.

Parameters
projectIdstringqueryrequiredProject ID
competitorDomainstringqueryrequiredCompetitor domain (e.g. competitor.com — normalised server-side)
startDatestringqueryYYYY-MM-DD
endDatestringqueryYYYY-MM-DD
Response 200
{
  "success": true,
  "keywordHistory": {
    "65f1c2a8e1234567890fffff": {
      "keyword": {
        "_id": "65f1c2a8e1234567890fffff",
        "value": "seo tools",
        "geoCode": "US",
        "langCode": "en"
      },
      "updates": [
        {
          "_id": "UPD_ID",
          "date": "2025-01-15T12:00:00.000Z",
          "status": "success",
          "position": {
            "position": 4,
            "domain": "competitor.com",
            "title": "-",
            "url": "-"
          },
          "aiSerpId": null
        }
      ]
    }
  }
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/competitor-positions?projectId=PROJECT_ID&competitorDomain=competitor.com"
GET/api/projects/competitors-filtered

Compare traffic and performance across a domain and its competitors

PUBLIC endpoint — no API key required. Rate limited: 60 req / min (600 / hour) per IP. Reads from the SimilarWeb / PageSpeed cache only (no live fetching). Auto-filters competitors to a balanced sample of 6 (3 above, 3 below main domain traffic) when more than 6 are supplied. `data` is keyed by domain.

Parameters
domainstringqueryrequiredMain domain
competitorsstringqueryrequiredComma-separated competitor domains, or repeated query param. Max 50 accepted (HTTP 400 above); auto-filtered to 6 in the response.
Response 200
{
  "success": true,
  "data": {
    "example.com": {
      "stats": {
        "domain": "example.com",
        "totalVisits": 1234567,
        "fetchedAt": "2025-01-16T08:00:00.000Z"
      },
      "monthlyVisits": [
        {
          "month": "2025-01-01",
          "visits": 1234567
        }
      ],
      "pageSpeed": {
        "cruxScore": 78
      }
    }
  },
  "filtered": [
    "competitor.com"
  ],
  "mainDomainVisits": 1234567,
  "totalCompetitors": 1
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/competitors-filtered?domain=example.com&competitors=a.com,b.com"

Performance

Page-level rankings and Core Web Vitals.

GET/api/projects/pages

Page-level ranking data: which URLs rank, average position, trend sparklines

Aggregated by the projectPagesPoller. Each page includes 14-point trend data for the last 90 days, bucketed weekly (or daily if there is not enough data for a week-bucket). Public when project is shared. Sorted worst-first by avgPosition.

Parameters
projectIdstringqueryrequiredProject ID
Response 200
{
  "success": true,
  "data": [
    {
      "url": "https://example.com/blog/post",
      "path": "/blog/post",
      "keywordCount": 12,
      "avgPosition": 8.4,
      "bestPosition": 3,
      "keywords": [
        {
          "keywordId": "KW_ID",
          "value": "seo tools",
          "position": 5
        }
      ],
      "avgPositionTrend": [
        {
          "date": "2025-01-06T00:00:00.000Z",
          "avgPosition": 9.2,
          "keywordCount": 12
        }
      ],
      "avgPositionTrendGranularity": "week",
      "weeklyAvgPositions": [
        {
          "weekStart": "2025-01-06T00:00:00.000Z",
          "avgPosition": 9.2,
          "keywordCount": 12
        }
      ]
    }
  ],
  "lastPagesUpdate": "2025-01-16T08:00:00.000Z"
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/pages?projectId=PROJECT_ID"
GET/api/projects/pagespeed

PageSpeed Insights (Core Web Vitals) for a domain

PUBLIC endpoint — no API key required. Rate limited: 60 req / min (600 / hour) per IP. Reads from DB cache only (never triggers a live PageSpeed run). Returns `data: null` if domain not in cache. Domain is normalised (strips `www.`, lowercased).

Parameters
domainstringqueryrequiredTarget domain
Response 200
{
  "success": true,
  "data": {
    "domain": "example.com",
    "performanceScore": 86,
    "accessibilityScore": 92,
    "bestPracticesScore": 90,
    "seoScore": 95,
    "cruxScore": 78,
    "largestContentfulPaint": 1.8,
    "firstContentfulPaint": 1,
    "cumulativeLayoutShift": 0.05,
    "totalBlockingTime": 120,
    "cruxFieldData": {
      "LCP": {
        "p75": 1900
      },
      "CLS": {
        "p75": 0.05
      },
      "INP": {
        "p75": 180
      }
    },
    "fetchedAt": "2025-01-16T08:00:00.000Z"
  }
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/pagespeed?domain=example.com"
GET/api/projects/pages-pagespeed

Per-page Lighthouse lab metrics for all tracked pages in a project

Public when project is shared. `pages` is keyed by URL. `baseline` is the home-page record when available, falling back to the domain-level snapshot (with `source: "domain"`) so the UI can still compute deltas. Returns `baseline: null` if neither exists.

Parameters
projectIdstringqueryrequiredProject ID
Response 200
{
  "success": true,
  "baseline": {
    "url": "https://example.com/",
    "path": "/",
    "domain": "example.com",
    "performanceScore": 86,
    "largestContentfulPaint": 1.8,
    "firstContentfulPaint": 1,
    "cumulativeLayoutShift": 0.04,
    "totalBlockingTime": 120,
    "source": "page"
  },
  "pages": {
    "https://example.com/blog/post": {
      "url": "https://example.com/blog/post",
      "path": "/blog/post",
      "performanceScore": 80,
      "largestContentfulPaint": 2.1,
      "source": "page"
    }
  }
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/pages-pagespeed?projectId=PROJECT_ID"
GET/api/projects/crux-history

Chrome UX Report (CrUX) real-user performance history for a domain

PUBLIC endpoint — no API key required. Rate limited: 60 req / min (600 / hour) per IP. Reads from DB cache only. `data` is the origin-level series (CrUX records with no specific URL); `pages` is per-page records keyed by URL with parsed `path`.

Parameters
domainstringqueryrequiredTarget domain
Response 200
{
  "success": true,
  "data": [
    {
      "domain": "example.com",
      "date": "2025-01-01",
      "lcp": {
        "p75": 1900
      },
      "cls": {
        "p75": 0.05
      }
    }
  ],
  "pages": [
    {
      "url": "https://example.com/blog/",
      "path": "/blog/",
      "data": [
        {
          "date": "2025-01-01",
          "lcp": {
            "p75": 2100
          }
        }
      ]
    }
  ]
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/crux-history?domain=example.com"
GET/api/projects/similarweb

SimilarWeb traffic stats and monthly visit history for a domain

PUBLIC endpoint — no API key required. Rate limited: 60 req / min (600 / hour) per IP. Reads from DB cache only. `exists: false` and `data: null` when the domain has never been scraped. `monthlyVisits` capped at 12 months, oldest first.

Parameters
domainstringqueryrequiredTarget domain
Response 200
{
  "success": true,
  "exists": true,
  "data": {
    "stats": {
      "domain": "example.com",
      "totalVisits": 1234567,
      "fetchedAt": "2025-01-16T08:00:00.000Z"
    },
    "monthlyVisits": [
      {
        "domain": "example.com",
        "month": "2024-02-01",
        "visits": 1100000
      },
      {
        "domain": "example.com",
        "month": "2025-01-01",
        "visits": 1234567
      }
    ]
  }
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/similarweb?domain=example.com"

Notes

Timeline annotations for a project.

GET/api/projects/notes

List timeline notes for a project

Parameters
projectIdstringqueryrequiredProject ID
Response 200
{
  "success": true,
  "data": [
    {
      "_id": "NOTE_ID",
      "date": "2025-01-15",
      "text": "Google algorithm update",
      "color": "warning"
    }
  ]
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/notes?projectId=PROJECT_ID"
POST/api/projects/notes201

Add or update a note on a project timeline

Upsert by `(projectId, date)`: if a note already exists for this date, it is updated in place and the response is HTTP 200 with `message: "Note updated"`. Otherwise a new note is created and the response is HTTP 201 with `message: "Note saved"`. Requires active subscription.

Parameters
projectIdstringbodyrequiredProject ID
datestringbodyrequiredYYYY-MM-DD (interpreted as 00:00:00 UTC)
textstringbodyrequiredNote content
colorenumbodyinfo | warning | success | danger (default: info, only set on new notes)
Response 201
{
  "success": true,
  "message": "Note saved",
  "data": {
    "_id": "NOTE_ID",
    "projectId": "PROJECT_ID",
    "date": "2025-01-15T00:00:00.000Z",
    "text": "Google algorithm update",
    "color": "warning"
  }
}
Example (cURL)
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID","date":"2025-01-15","text":"Google algorithm update","color":"warning"}' \
  https://serpdino.com/api/projects/notes
DELETE/api/projects/notes

Delete a note

Returns the remaining notes for the project in `data` after deletion. Requires active subscription.

Parameters
noteIdstringbodyrequiredNote ID
Response 200
{
  "success": true,
  "message": "Note deleted",
  "data": [
    {
      "_id": "OTHER_NOTE_ID",
      "date": "2025-01-10T00:00:00.000Z",
      "text": "...",
      "color": "info"
    }
  ]
}
Example (cURL)
curl -X DELETE \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"noteId":"NOTE_ID"}' \
  https://serpdino.com/api/projects/notes

Account

Check usage and limits.

GET/api/user/capacity

Check account usage and limits

Response 200
{
  "total": 1000,
  "booked": 320,
  "available": 680,
  "projects": 4,
  "keywords": 320,
  "isTrial": false
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  https://serpdino.com/api/user/capacity

Export

Export project data as Markdown or CSV.

GET/api/projects/export-agent

Generate a comprehensive Markdown report for a project

Returns plain Markdown text (Content-Type: text/markdown), NOT JSON. Includes rankings, traffic, competitors, and performance data in a single document — designed for feeding to LLMs.

Parameters
projectIdstringqueryrequiredProject ID
Response 200 · text/markdown
# Acme Corp — SEO Report

Domain: acme.com

## Rankings
...
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/export-agent?projectId=PROJECT_ID"
GET/api/projects/export

CSV export of ranking data

Returns CSV with UTF-8 BOM and semicolon (`;`) separator — opens correctly in Excel without conversion. `Content-Disposition` header forces download with a generated filename. `full` format: one row per keyword with a date column per checked day. `summary` format: one row per keyword with aggregated stats (current/best/worst/avg position, change). Public when project is shared.

Parameters
projectIdstringqueryrequiredProject ID
formatenumqueryfull | summary (default: full)
startDatestringqueryYYYY-MM-DD
endDatestringqueryYYYY-MM-DD
Response 200 · text/csv; charset=utf-8
Keyword;SERP;Volume;URL;Change;2025-01-16;2025-01-15
seo tools;en-US;8100;https://example.com/seo-tools;-3;9;12
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/export?projectId=PROJECT_ID&format=full" -o report.csv

API Keys

Create and revoke API keys programmatically.

GET/api/user/api-keys

List your API keys

Returns metadata only — secrets are never returned after creation. Each entry includes _id, name, keyPrefix (masked, first 4 chars only), lastUsedAt, createdAt.

Response 200
{
  "success": true,
  "apiKeys": [
    {
      "_id": "KEY_ID",
      "name": "My Integration",
      "keyPrefix": "sd_a••••",
      "lastUsedAt": "2025-01-16T10:00:00.000Z",
      "createdAt": "2025-01-10T08:00:00.000Z"
    }
  ]
}
Example (cURL)
curl -H "Authorization: Bearer sd_your_key_here" \
  https://serpdino.com/api/user/api-keys
POST/api/user/api-keys201

Create a new API key

`key` is the full secret — shown only once, store it immediately. `apiKey` is metadata. Max 5 active keys per user. Names limited to 100 chars.

Parameters
namestringbodyrequiredKey name (max 100 chars)
Response 201
{
  "success": true,
  "key": "sd_abcd1234567890abcdef1234567890abcdef1234",
  "apiKey": {
    "_id": "KEY_ID",
    "name": "My Integration",
    "keyPrefix": "sd_a••••",
    "createdAt": "2025-01-16T10:00:00.000Z"
  }
}
Example (cURL)
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name":"My Integration"}' \
  https://serpdino.com/api/user/api-keys
DELETE/api/user/api-keys

Revoke an API key

Soft-revokes by setting `revokedAt`. Accepts `id` from either body or query string. Returns 404 if the key does not exist or already revoked.

Parameters
idstringbodyAPI key ID (one of body or query is required)
idstringqueryAPI key ID (one of body or query is required)
Response 200
{
  "success": true
}
Example (cURL)
curl -X DELETE \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"id":"KEY_ID"}' \
  https://serpdino.com/api/user/api-keys