Pular para o conteúdo

01 REST API reference

Every number in your dashboard, over HTTP.

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

v1.0.046 endpointsOpenAPI 3.1Bearer auth

Authentication

Every request carries your API key as a bearer token. No OAuth dance, no session to keep alive, no signing. The base URL is https://serpdino.com.

Request
GET https://serpdino.com/api/projects
Authorization: Bearer sd_your_key_here
Keys live in the dashboard

Create and revoke keys in Dashboard → Settings → API Keys. Up to five active keys per account. API access is a paid-plan feature: a key issued while subscribed stops working the moment the account drops back to Free.

Use with AI agents (MCP)

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

MCP endpoint
https://serpdino.com/api/mcp

Works with Claude (web & desktop), ChatGPT (Developer mode), Cursor, Claude Code, and any client speaking the Model Context Protocol. The MCP setup guide has per-client instructions; the serpdino-mcp repo covers the local install.

Quick start

Three calls from empty account to a ranking history you can graph.

01 — List your projects

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

02 — 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

03 — Read the rankings back

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/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.
  • Every endpoint on this API requires an active paid subscription. The Free plan has full access to the dashboard but not to the API: a key issued while the account was subscribed stops working the moment it drops to Free, and returns HTTP 403 { message: "The Free plan does not include API access…", planRequired: true }.
  • 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 require an API key (or dashboard session): /api/projects/competitors-filtered, /api/projects/pagespeed, /api/projects/crux-history, /api/projects/similarweb. They read from our DB cache only — no live upstream calls. Each is rate-limited to 60 req / min (600 / hour) per account, plus a daily cap of 400 new-to-us domains. Cached domains never count against that cap.
Typical error body
{ "success": false, "message": "Description of what went wrong" }
StatusMeaningWhen
400Bad RequestMissing required parameter or invalid value
401UnauthorizedMissing or invalid API key. Body: { message } or { success: false }
403ForbiddenNo API access on the current plan (Free), or the account is blocked. Body: { message, planRequired: 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 allows 10 req / 60s per API key, /api/tools/traffic-check 20 req / 60s. /api/projects/keyword-ideas allows 15 req / min (150 / hour) per account. competitors-filtered / pagespeed / crux-history / similarweb allow 60 req / min (600 / hour) per account, plus a daily quota of 400 new-to-us domains. 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

  • id string · path · required — Project 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/projects

Create a new project → 201

Returns HTTP 201 on success. Fires fire-and-forget triggers for keyword suggestions, SimilarWeb, PageSpeed, and competitor analysis. For a Google Maps geo-grid project pass type: "local" with business and localGrid instead of domain — see the Local section.

Parameters

  • name string · body · required — Project name
  • type enum · body — serp | local | monitor (default: serp)
  • domain string · body — Target domain (e.g. example.com). Lowercased server-side. Required for serp and monitor projects; not used by local projects.
  • business object · body — Local projects only, required there: the business object returned by GET /api/local/business-details (needs at least placeId, name, numeric lat/lng).
  • localGrid object · body — Local projects only: { mode: "grid" | "circle", size, stepKm } centred on the business, or { mode: "custom", points: [{ lat, lng }] }. Default: circle, size 5, 1 km.
  • folder string · body — Folder ID to place project in
  • icon string · body — Icon identifier or URL
  • aliases string · body — Comma/newline-separated alternative domains (treated as same property)
  • competitors string[] · body — Competitor domains (PUT only — POST ignores this field)
  • shared boolean · body — Whether project is shared via public link (default: false)
  • updateFrequency enum · body — daily | every3days | weekly | biweekly | custom | ondemand (default: daily). ondemand never runs on its own — checks run only via POST /api/scrape/new-keywords.
  • customUpdateDays number[] · body — Required when updateFrequency=custom. Array of weekday numbers 0–6 (0=Sunday).
  • updateTime number · body — Hour of day 0–23 to run updates (default: 12)
  • updateTimezone string · body — IANA timezone name (default: UTC)
  • serpTop number · body — 50 | 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 and name are always required, domain too except on local projects (the handler rejects with 400 if any is missing). Settings you omit are reset to their defaults (folder, icon, schedule, timezone, sharing) — to change a single field, send the existing values for the others. A project's type never changes; a local project's grid is edited with PUT /api/projects/local-grid.

Parameters

  • _id string · body · required — Project ID
  • name string · body · required — Project name (required even if unchanged)
  • domain string · body — Target domain (required even if unchanged, except on local projects)
  • competitors string[] · body — Array of competitor domains
  • folder string · body — Folder ID, or empty string to remove from folder
  • icon string · body — Icon identifier or URL
  • aliases string · body — Comma/newline-separated alias domains
  • shared boolean · body — Public sharing toggle
  • updateFrequency enum · body — daily | every3days | weekly | biweekly | custom | ondemand
  • customUpdateDays number[] · body — Required when updateFrequency=custom. Weekday numbers 0–6.
  • updateTime number · body — Hour 0–23
  • updateTimezone string · body — IANA timezone
  • serpTop number · body — 50 | 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

  • _id string · body · required — Project 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/folders

Create folder → 201

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

  • name string · body · required — Folder 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

  • id string · query · required — Folder ID
  • name string · body · required — New 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

  • id string · query · required — Folder 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

  • projectId string · body · required — Project ID
  • keywords string[] · body · required — Array of keyword strings (lowercased server-side)
  • geoCode string · body · required — ISO-2 country code (e.g. US). Required — no default.
  • langCode string · body · required — ISO-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

  • projectId string · body · required — Project ID
  • keywordIds string[] · body · required — Array 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 check for keywords (SERP or Google Maps grid)

Async — returns immediately (HTTP 200, status: "processing") while scraping happens in the background. Pass all project keyword IDs to refresh everything. Requires active subscription. All keywordIds must belong to the given project. Credits. One check = one keyword on a serp project, or one keyword × one grid point on a local project. 1 credit is charged per check only when it completes successfully; failed checks are retried automatically (up to 5 times) at no charge. Rejects with HTTP 400 "Insufficient balance" if the balance is below the number of checks. Repeat calls. Checks still queued or running are never queued twice — they are reported in checks.alreadyQueued, so retrying this call after a timeout is safe while the run is in progress. Once a run has finished, a new call is a new paid run.

Parameters

  • projectId string · body · required — Project ID
  • keywordIds string[] · body · required — Keyword 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
  },
  "checks": {
    "queued": 10,
    "alreadyQueued": 0,
    "failedToQueue": 0,
    "maxCredits": 10
  }
}
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

  • projectId string · query · required — Project ID
  • startDate string · query — YYYY-MM-DD (default: 30 days ago)
  • endDate string · query — YYYY-MM-DD (default: today)
  • aggregation enum · query — daily | weekly | monthly (default: daily)
  • search string · query — Filter keywords by substring (case-insensitive)
  • geoCode string · query — Filter by country code (uppercased)
  • langCode string · query — Filter by language code (lowercased)
  • sortBy enum · query — keyword | position | volume
  • sortOrder enum · query — asc | 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

  • keywordUpdateId string · query · required — Keyword 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

  • projectId string · query · required — Project ID
  • keywordId string · query — Single 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/serp-check

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

Rate limited: 10 req / 60s per API user — all keys on one account share the bucket. 429 response body includes retryAfter (seconds), limit, window. Returns HTTP 502 if both upstream SERP and volume calls fail. Search volume is served from Serpdino's own keyword database, matched on keyword + country: if we have no data for that pair, volume.metrics is null and volume.volume is [] while the SERP part is still returned in full.

Parameters

  • keyword string · body · required — Search query
  • geo string · body · required — ISO-2 country code (e.g. US)
  • lang string · body · required — ISO-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": 9
}
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: 20 req / 60s per API user — all keys on one account share the bucket. A daily quota of 400 new-to-us domains also applies — cached domains never count against it. 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

  • domain string · body · required — Target 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": 19
}
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

  • projectId string · query · required — Project 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: 15 req / min, 150 req / hour per account — HTTP 429 with retryAfter when exceeded.

Parameters

  • projectId string · body · required — Project ID
  • geo string · body — Country code (default: US, uppercased)
  • lang string · body — Language 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

Local (Google Maps geo-grid)

Track a Google Maps business across a grid of GPS points. Flow: resolve the business with `GET /api/local/business-details`, create a project with `type: "local"` (`POST /api/projects`), add keywords (`POST /api/projects/keywords`), start a scan (`POST /api/scrape/new-keywords`), poll `GET /api/projects/local-progress`, then read results. Each keyword × grid point is one check costing 1 credit, charged only on success — a 5×5 grid with 3 keywords is 75 credits per scan. Create the project with `updateFrequency: "ondemand"` to run scans only when you call the API: no recurring scans and no automatic first scan. Results are stored per UTC day; scanning the same keyword and point again on the same day replaces that day's result and is charged again.

GET/api/local/business-details

Resolve a Google Place ID into the business object a local project needs

Live Google Places lookup. Pass the returned business object unchanged to POST /api/projects. lat/lng are the grid centre for grid/circle layouts. countryCode/langCode are the defaults used for scans when a keyword has no locale of its own. Rate limited: 15 req / min, 150 / hour per account.

Parameters

  • placeId string · query · required — Google Place ID (ChIJ…)
  • language string · query — Language for the localized name/address, e.g. en
Response 200
{
  "success": true,
  "business": {
    "placeId": "ChIJN1t_tDeuEmsRUsoyG83frY4",
    "name": "Acme Dental",
    "address": "123 Main St, Springfield, IL 62701, USA",
    "lat": 39.7817,
    "lng": -89.6501,
    "serviceArea": false,
    "rating": 4.7,
    "reviewCount": 312,
    "website": "https://acmedental.example",
    "mapsUri": "https://maps.google.com/?cid=…",
    "countryCode": "US",
    "langCode": "EN"
  }
}
Example · cURL
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/local/business-details?placeId=ChIJN1t_tDeuEmsRUsoyG83frY4"
PUT/api/projects/local-grid

Replace the grid of a local project

grid is a size × size square and circle is concentric rings; both are centred on the business. Size: 3–21. stepKm: 0.5–16.09 (10 mi). custom takes your own points, up to 60, and is how you scan around a centre other than the business. The response contains the materialized localGrid.points. Results reference points by pointIndex, their position in that array, so changing the grid changes what a pointIndex means.

Parameters

  • projectId string · body · required — Local project ID
  • localGrid object · body · required — { mode: "grid" | "circle", size, stepKm, unit? } or { mode: "custom", points: [{ lat, lng }] }. unit (km | mi) is display-only; stepKm is always in km.
Response 200
{
  "success": true,
  "project": {
    "_id": "PROJECT_ID",
    "type": "local",
    "localGrid": {
      "mode": "grid",
      "size": 5,
      "stepKm": 1,
      "unit": "km",
      "points": [
        {
          "lat": 39.763714,
          "lng": -89.673512
        },
        "… 25 points"
      ]
    }
  }
}
Example · cURL
curl -X PUT \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID","localGrid":{"mode":"grid","size":5,"stepKm":1}}' \
  https://serpdino.com/api/projects/local-grid
GET/api/projects/local-progress

Scan progress of a local project

Lightweight, so it is safe to poll every 10–15 seconds. pending has one entry per keyword with checks still queued, running or waiting for an automatic retry. The scan is finished when scanning is false. failed lists points that exhausted their retries today (UTC). Those points have no result, were not charged and will not be retried unless you start a new scan.

Parameters

  • projectId string · query · required — Local project ID
Response 200
{
  "success": true,
  "totalPoints": 25,
  "scanning": true,
  "pending": {
    "KW_ID_1": {
      "pending": 7,
      "done": 18,
      "total": 25
    }
  },
  "failed": {
    "KW_ID_2": {
      "count": 1,
      "pointIndexes": [
        14
      ]
    }
  }
}
Example · cURL
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/local-progress?projectId=PROJECT_ID"
GET/api/projects/local-keyword-grid

Your rank at every grid point for one keyword

position is the business's rank in the Google Maps list at that point; 0 means it was not in the list. Defaults to the latest scanned day. Public when the project is shared.

Parameters

  • projectId string · query · required — Local project ID
  • keywordId string · query · required — Keyword ID
  • date string · query — YYYY-MM-DD (UTC). Default: latest scanned day for this keyword
Response 200
{
  "success": true,
  "date": "2026-10-16",
  "points": [
    {
      "pointIndex": 0,
      "lat": 39.763714,
      "lng": -89.673512,
      "position": 4
    },
    {
      "pointIndex": 1,
      "lat": 39.763714,
      "lng": -89.661796,
      "position": 0
    }
  ]
}
Example · cURL
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/local-keyword-grid?projectId=PROJECT_ID&keywordId=KW_ID_1"
GET/api/projects/local-point

Full Google Maps ranking at one grid point: every competitor, per keyword

Every listing Google Maps showed at that point, typically up to 40 per keyword, with rank, Place ID, name, category, rating, review count, address and website. isOurs marks the tracked business and its branch listings. Defaults to the latest scanned day. Public when the project is shared.

Parameters

  • projectId string · query · required — Local project ID
  • pointIndex number · query · required — Index into localGrid.points
  • keywordId string · query — Only this keyword
  • date string · query — YYYY-MM-DD (UTC). Default: latest scanned day for this point
Response 200
{
  "success": true,
  "date": "2026-10-16",
  "point": {
    "pointIndex": 12,
    "lat": 39.7817,
    "lng": -89.6501
  },
  "keywords": [
    {
      "keywordId": "KW_ID_1",
      "value": "dentist",
      "ourPosition": 3,
      "results": [
        {
          "position": 1,
          "placeId": "ChIJ…",
          "isOurs": false,
          "name": "Springfield Smiles",
          "category": "Dentist",
          "rating": 4.9,
          "reviewCount": 841,
          "address": "…",
          "photoUrl": null,
          "website": "https://…"
        },
        {
          "position": 3,
          "placeId": "ChIJN1t_tDeuEmsRUsoyG83frY4",
          "isOurs": true,
          "name": "Acme Dental",
          "category": "Dentist",
          "rating": 4.7,
          "reviewCount": 312,
          "address": "…",
          "photoUrl": null,
          "website": "https://acmedental.example"
        }
      ]
    }
  ]
}
Example · cURL
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/local-point?projectId=PROJECT_ID&pointIndex=12"
GET/api/projects/local-map

Share of Local Voice history per keyword plus the latest grid map

data holds one entry per keyword with an update per scanned day. There position.position is the SoLV %: the share of grid points where the business ranks in the top 20. map describes the latest day per point: visibility %, average rank position (arp, null when absent everywhere) and the raw position for each keyword. While a scan is running, its partial day is hidden for keywords that already have an earlier finished day. Public when the project is shared.

Parameters

  • projectId string · query · required — Local project ID
Response 200
{
  "success": true,
  "data": {
    "KW_ID_1": {
      "keyword": {
        "_id": "KW_ID_1",
        "value": "dentist",
        "geoCode": "US",
        "langCode": "en"
      },
      "updates": [
        {
          "date": "2026-10-16T09:12:44.000Z",
          "position": {
            "position": 64
          },
          "solv": {
            "inTopN": 16,
            "total": 25,
            "topN": 20
          },
          "status": "success"
        }
      ]
    }
  },
  "map": {
    "latestDate": "2026-10-16",
    "topN": 20,
    "keywords": [
      {
        "id": "KW_ID_1",
        "value": "dentist"
      }
    ],
    "points": [
      {
        "pointIndex": 0,
        "lat": 39.763714,
        "lng": -89.673512,
        "vis": {
          "pct": 71,
          "total": 3
        },
        "solv": {
          "pct": 67,
          "inTopN": 2,
          "total": 3,
          "topN": 20
        },
        "arp": 5.5,
        "found": 2,
        "total": 3,
        "keywords": [
          {
            "keywordId": "KW_ID_1",
            "position": 4
          }
        ]
      }
    ]
  },
  "dates": [
    "2026-10-16"
  ]
}
Example · cURL
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/local-map?projectId=PROJECT_ID"

Page Monitor

Check your own pages on every update: whether Google has them indexed, HTTP status and redirects, meta robots, canonical, indexability and load speed. Create a project with `type: "monitor"` and `domain` (`POST /api/projects`), then add pages. 1 credit per page per check covers the whole check, charged only when it completes; failed checks are retried automatically at no charge. On a scheduled project a newly added page gets its first check within about a minute, which is charged. Use `updateFrequency: "ondemand"` to check only via `POST /api/scrape/monitor-run`. Readings are stored one per page per UTC day; checking a page again the same day replaces that day's reading. Indexing is checked on google.com (US, English) with a `site:` query. The `X-Robots-Tag` response header is not read, so a noindex sent only in that header is not detected.

POST/api/projects/monitor-pages

Add pages to a monitor project

Only URLs on the project domain or its subdomains are accepted. Up to 1000 pages per project. The URL is normalized for duplicate detection. Rejected lines are reported in skipped (the first 20 of each kind, plus counts).

Parameters

  • projectId string · body · required — Monitor project ID
  • urls string[] · body · required — Page URLs; a single string with one URL per line (or comma/space separated) also works
Response 200
{
  "success": true,
  "addedCount": 2,
  "skipped": {
    "invalid": [],
    "invalidCount": 0,
    "foreign": [
      "https://other.com/"
    ],
    "foreignCount": 1,
    "duplicate": 0,
    "overLimit": 0
  },
  "project": {
    "_id": "PROJECT_ID",
    "type": "monitor",
    "domain": "example.com",
    "pageCount": 2
  }
}
Example · cURL
curl -X POST \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID","urls":["https://example.com/","https://example.com/pricing"]}' \
  https://serpdino.com/api/projects/monitor-pages
PUT/api/projects/monitor-pages

Set a page note or reorder pages

Send pageId + note (empty string clears it, max 300 chars), or order with page IDs in the new order. A page URL cannot be edited: remove the page and add the new URL.

Parameters

  • projectId string · body · required — Monitor project ID
  • pageId string · body — Page to annotate
  • note string · body — Note text
  • order string[] · body — Page IDs in display order
Response 200
{
  "success": true
}
Example · cURL
curl -X PUT \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID","pageId":"PAGE_ID","note":"Landing for the spring campaign"}' \
  https://serpdino.com/api/projects/monitor-pages
DELETE/api/projects/monitor-pages

Remove pages and their history

Also drops their queued checks and every stored reading.

Parameters

  • projectId string · body · required — Monitor project ID
  • pageIds string[] · body · required — Pages to remove
Response 200
{
  "success": true,
  "deletedCount": 1,
  "project": {
    "_id": "PROJECT_ID",
    "pageCount": 1
  }
}
Example · cURL
curl -X DELETE \
  -H "Authorization: Bearer sd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID","pageIds":["PAGE_ID"]}' \
  https://serpdino.com/api/projects/monitor-pages
POST/api/scrape/monitor-run

Check pages now

Async: returns at once while checks run in the background — poll monitor-checks until pending is empty. Omit pageIds to check the whole project. Rejects with HTTP 400 "Insufficient balance" (with required and available) before anything is queued. Pages already queued or running are not queued again; they are counted in summary.skipped, so repeating the call after a timeout is safe while the run is in progress. Once it has finished, a new call is a new paid run. HTTP 409 if the project is suspended.

Parameters

  • projectId string · body · required — Monitor project ID
  • pageIds string[] · body — Only these pages (default: all)
Response 200
{
  "success": true,
  "status": "processing",
  "summary": {
    "queued": 12,
    "skipped": 0,
    "errors": 0,
    "runs": 12
  }
}
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/scrape/monitor-run
GET/api/projects/monitor-checks

Pages with their latest state and daily readings

Every page comes back with last, a copy of its newest reading, so startDate=endDate=<today> is the cheap way to get the current state of a large project. checks is keyed by page ID, then by UTC day. pending marks pages with a check queued or running: poll until it is empty. failed marks pages whose check today exhausted its retries; they have no reading for today and were not charged. index.state: indexed | not_indexed | indexed_other (Google shows a different URL, e.g. the canonical) | error. seo.indexable is null when the page was behind bot protection (http.blocked). Speed is in ms, weight in bytes. Public when the project is shared.

Parameters

  • projectId string · query · required — Monitor project ID
  • startDate string · query — YYYY-MM-DD, UTC (default: 30 days before endDate; range capped at 400 days)
  • endDate string · query — YYYY-MM-DD, UTC (default: today)
  • pageIds string · query — Comma-separated page IDs to limit the response to
Response 200
{
  "success": true,
  "range": {
    "startDate": "2026-10-15",
    "endDate": "2026-10-16"
  },
  "days": [
    "2026-10-15",
    "2026-10-16"
  ],
  "pages": [
    {
      "_id": "PAGE_ID",
      "url": "https://example.com/pricing",
      "normalizedUrl": "https://example.com/pricing",
      "position": 1,
      "note": "Landing for the spring campaign",
      "last": {
        "runDate": "2026-10-16T00:00:00.000Z",
        "statusCode": 200,
        "redirected": false,
        "blocked": false,
        "indexState": "indexed",
        "indexable": true,
        "metaRobots": "index, follow",
        "canonical": "https://example.com/pricing",
        "canonicalMatch": true,
        "ttfb": 212,
        "load": 1840,
        "fcp": 690,
        "weight": 1482331
      },
      "createdDate": "2026-10-01T09:00:00.000Z"
    }
  ],
  "checks": {
    "PAGE_ID": {
      "2026-10-16": {
        "http": {
          "statusCode": 200,
          "finalUrl": "https://example.com/pricing",
          "redirected": false,
          "blocked": false,
          "checkedAt": "2026-10-16T09:00:04.000Z"
        },
        "seo": {
          "title": "Pricing — Example",
          "metaRobots": "index, follow",
          "canonical": "https://example.com/pricing",
          "canonicalMatch": true,
          "indexable": true
        },
        "speed": {
          "ttfb": 212,
          "dcl": 540,
          "load": 1840,
          "fcp": 690,
          "weight": 1482331,
          "samples": [
            {
              "ttfb": 212,
              "dcl": 540,
              "load": 1840,
              "fcp": 690,
              "weight": 1482331,
              "region": "eu-west-2"
            }
          ]
        },
        "index": {
          "state": "indexed",
          "matchedUrl": "https://example.com/pricing",
          "via": "site",
          "results": 10,
          "checkedAt": "2026-10-16T09:00:05.000Z"
        }
      }
    }
  },
  "pending": {},
  "failed": {}
}
Example · cURL
curl -H "Authorization: Bearer sd_your_key_here" \
  "https://serpdino.com/api/projects/monitor-checks?projectId=PROJECT_ID&startDate=2026-10-01&endDate=2026-10-16"

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

  • projectId string · query · required — Project ID
  • competitorDomain string · query · required — Competitor domain (e.g. competitor.com — normalised server-side)
  • startDate string · query — YYYY-MM-DD
  • endDate string · query — YYYY-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

Requires an API key or dashboard session (401 otherwise). Rate limited: 60 req / min (600 / hour) per identity, plus a daily new-domain quota. 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

  • domain string · query · required — Main domain
  • competitors string · query · required — Comma-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

  • projectId string · query · required — Project 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

Requires an API key or dashboard session (401 otherwise). Rate limited: 60 req / min (600 / hour) per identity, plus a daily new-domain quota. 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

  • domain string · query · required — Target 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

  • projectId string · query · required — Project 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

Requires an API key or dashboard session (401 otherwise). Rate limited: 60 req / min (600 / hour) per identity, plus a daily new-domain quota. 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

  • domain string · query · required — Target 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

Requires an API key or dashboard session (401 otherwise). Rate limited: 60 req / min (600 / hour) per identity, plus a daily new-domain quota. 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

  • domain string · query · required — Target 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

  • projectId string · query · required — Project 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/notes

Add or update a note on a project timeline → 201

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

  • projectId string · body · required — Project ID
  • date string · body · required — YYYY-MM-DD (interpreted as 00:00:00 UTC)
  • text string · body · required — Note content
  • color enum · body — info | 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

  • noteId string · body · required — Note 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

The one place to read every limit that applies to your account, so a client can pace itself instead of discovering limits by being rejected. total / booked / available are monthly SERP-check tokens — the budget consumed by tracking keywords, calculated as keywords × refreshes per month (× 2 when serpTop is 100). There is no limit on the number of projects (websites) — limits.projects is null, meaning unlimited. Running out of tokens is what stops you adding more tracking, not a website count. limits.rateLimits reports the per-API-key ceilings enforced on the endpoints of the same name; domainReads covers competitors-filtered / pagespeed / crux-history / similarweb. limits.rateLimits.newDomainsPerDay is the odd one out: not a per-minute rate but a daily allowance of domains that are new to us, and a single bucket shared by every endpoint in its appliesTo list rather than one allowance each. Domains already in our cache never consume it, so re-checking a domain you looked up last week is free. resetsInSeconds is the time until the allowance clears if you look up no further new domains — each newly counted domain re-arms the 24h window — and is null when nothing has been counted yet. limits is always read live and never served from cache.

Response 200
{
  "total": 12000,
  "booked": 320,
  "available": 11680,
  "projects": 4,
  "keywords": 320,
  "plan": "paid",
  "limits": {
    "keywordsPerProject": 3500,
    "projects": null,
    "apiKeys": {
      "limit": 5,
      "used": 2
    },
    "competitorsPerRequest": 50,
    "rateLimits": {
      "serpCheck": {
        "perMinute": 10,
        "windowSeconds": 60
      },
      "trafficCheck": {
        "perMinute": 20,
        "windowSeconds": 60
      },
      "keywordResearch": {
        "perMinute": 15,
        "perHour": 150
      },
      "domainReads": {
        "perMinute": 60,
        "perHour": 600
      },
      "newDomainsPerDay": {
        "limit": 400,
        "used": 37,
        "remaining": 363,
        "resetsInSeconds": 61840,
        "appliesTo": [
          "/api/tools/traffic-check",
          "/api/projects/similarweb",
          "/api/projects/pagespeed",
          "/api/projects/crux-history",
          "/api/projects/competitors-filtered"
        ]
      }
    }
  }
}
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

  • projectId string · query · required — Project 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). top10 format: a block per keyword with positions 1–10 as rows and the ranking URL for each checked day as columns, limited to the 90 most recent checked days. Public when project is shared.

Parameters

  • projectId string · query · required — Project ID
  • format enum · query — full | summary | top10 (default: full)
  • startDate string · query — YYYY-MM-DD
  • endDate string · query — YYYY-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-keys

Create a new API key → 201

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

  • name string · body · required — Key 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

  • id string · body — API key ID (one of body or query is required)
  • id string · query — API 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