REST API
Read projects, tracked keywords, groups, site totals, breakdowns, pages, queries, alerts and notes as JSON with a workspace API key.
The REST API returns the data you see in Serplyze as JSON, so you can feed a dashboard, a spreadsheet script or your own tools. It is read-only: every endpoint is a GET, and a key cannot change anything in the workspace.
For Google Sheets without any code, use live CSV links instead.
The API needs a paid plan. A key that belongs to a workspace on Free answers every request with 403 plan_required until the workspace is upgraded again. See Plans and billing.
Base URL
https://serplyze.com/api/v1All paths below are relative to this base. Responses are JSON and are sent with Cache-Control: no-store.
Authentication
Send the key in the Authorization header as a bearer token. There is no other way to pass it: query-string keys are not read.
curl https://serplyze.com/api/v1/projects \
-H "Authorization: Bearer ord_live_your_key"- A key starts with
ord_live_followed by 32 letters and digits. - A key belongs to one workspace and reads every project in it, whoever created the key.
- A missing key, a malformed key or a revoked key gets
401 unauthorizedwith aWWW-Authenticate: Bearerheader.
Create and revoke keys
- Open Settings → API and Sheets.
- Select Create API key, give it a name that says what it is for, and select Create key.
- Copy the key from the Copy your API key dialog. This is the only time it is shown: Serplyze stores a hash of the key and its first characters, not the key itself.
- Only workspace owners and admins can create or revoke keys. Other members see the list without the controls.
- The list shows each key's name, its first characters, when it was created and when it was last used.
- To revoke, select Revoke next to the key and confirm with Revoke key. The key stops working at once and cannot be restored.
- A workspace can have up to 20 active keys.
Rate limits
Each key may make 120 requests a minute. Every authenticated response carries three headers:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute (120). |
X-RateLimit-Remaining | Requests left in the current minute. |
X-RateLimit-Reset | Seconds until the current minute window ends. |
Over the limit, the API answers 429 with code rate_limited and a Retry-After header holding the number of seconds to wait.
{
"error": {
"code": "rate_limited",
"message": "Rate limit of 120 requests a minute reached."
}
}Requests without a valid key are limited separately: after 30 failed requests in a minute from one client address, further failed requests get 429 instead of 401.
Errors
Every error has the same shape: an error object with a machine-readable code and a message you can show to a person.
{
"error": {
"code": "invalid_request",
"message": "`period` must be days (e.g. 28d, up to 486d) or months (e.g. 3m, up to 16m)."
}
}| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A parameter is missing, malformed or out of range. The message names the parameter. |
| 401 | unauthorized | No key, or the key is not valid or was revoked. |
| 403 | plan_required | The workspace is on Free. |
| 404 | not_found | Unknown project or group id, or a project that has not finished setup. |
| 409 | google_access_lost | Pages and queries only: Google access to the property was revoked. Connect Search Console again in the app. |
| 429 | rate_limited | Too many requests. Wait for Retry-After seconds. |
| 500 | server_error | Something failed on our side. Try again later. |
| 503 | unavailable | Pages and queries only: Search Console could not be reached. Sent with Retry-After: 60. |
Ids
Project, group, page group and keyword ids are the short ids you see in app URLs: five letters and digits. Alert and note ids are UUIDs. Get project ids from GET /projects.
Date ranges
The keywords, site, breakdowns, pages and queries endpoints take the same range parameters. Ranges are inclusive.
| Parameter | Type | Default | Notes |
|---|---|---|---|
to | date YYYY-MM-DD | The project's last synced day (syncedThrough), or yesterday (UTC) before the first sync | Last day of the range. |
from | date YYYY-MM-DD | 27 days before to (a 28-day range) | First day of the range. Must not be after to. |
period | 1d to 486d, or 1m to 16m | none | A relative range that ends at to, in days or calendar months. Use it instead of from; sending both is a 400. |
- A range may span at most 486 days (about 16 months, which is what Search Console keeps).
- Keyword data is limited to the keyword history of your plan. If
fromis earlier, it is moved to the first available day and the response says"clamped": true. If the whole range is earlier, you get a400. - Site totals, breakdowns, pages and queries are not limited by plan history.
provisionalAfterin a response is the last final day. Later days are still being revised by Google and can change.
Pagination
List endpoints take limit and offset and return a paging object with total, limit and offset. Ask for the next page by adding limit to offset until offset reaches total.
| Endpoint | Default `limit` | Maximum `limit` | `offset` |
|---|---|---|---|
/projects/:id/keywords | 1000 | 5000 | yes |
/projects/:id/pages, /projects/:id/queries | 1000 | 25000 | yes |
/projects/:id/alerts | 100 | 500 | no |
The other endpoints return everything in one response.
Metrics
clicksandimpressionsare whole numbers.ctris a ratio from 0 to 1 with four decimals (0.0412is 4.12%). It isnullwhen there are no impressions.positionis the average position weighted by impressions, with one decimal. It isnullwhen there are no impressions.
GET /projects
The projects of the workspace, oldest first. Projects that have not finished setup are not listed. No parameters.
{
"projects": [
{
"id": "Ab12C",
"domain": "example.com",
"property": "sc-domain:example.com",
"country": "deu",
"language": "de",
"syncStatus": "ok",
"lastSyncedAt": "2026-03-14T05:12:44.000Z",
"syncedThrough": "2026-03-12",
"keywordHistoryFrom": "2025-09-13",
"historyBackfilling": false,
"trackedKeywords": 412
}
]
}countryis the project's country filter as an ISO 3166-1 alpha-3 code in lower case, ornullfor all countries.languageis a two-letter code ornull.syncStatusis one ofpending,ok,warning,failed.keywordHistoryFromis the first day of keyword data this workspace's plan can read.historyBackfillingistruewhile older days inside that window are still loading.trackedKeywordscounts keywords that are not archived.
GET /projects/:id/keywords
Tracked keywords with their totals over a date range, sorted by query text.
| Parameter | Type | Default | Allowed values |
|---|---|---|---|
from, to, period | see Date ranges | last 28 days | Clamped to the plan's keyword history. |
device | string | all | all, desktop, mobile, tablet |
daily | flag | off | 1 or true adds a daily array to every keyword. |
group | group id | none | Only keywords of this group: the members of a manual group, everything below a folder, or the matches of a rule group evaluated on the requested range. Unknown id: 404. |
limit | integer | 1000 | 1 to 5000 |
offset | integer | 0 | 0 or more |
curl "https://serplyze.com/api/v1/projects/Ab12C/keywords?period=7d&device=mobile&daily=1&limit=1" \
-H "Authorization: Bearer ord_live_your_key"{
"project": { "id": "Ab12C", "domain": "example.com" },
"range": {
"from": "2026-03-06",
"to": "2026-03-12",
"device": "mobile",
"clamped": false,
"provisionalAfter": "2026-03-10"
},
"paging": { "total": 412, "limit": 1, "offset": 0 },
"keywords": [
{
"id": "k7Qp2",
"query": "running shoes",
"source": "manual",
"trackedSince": "2025-11-02T09:30:00.000Z",
"intent": "commercial",
"groupIds": ["Gr4Zx"],
"metrics": { "clicks": 84, "impressions": 2310, "ctr": 0.0364, "position": 6.2 },
"daily": [
{ "date": "2026-03-06", "clicks": 11, "impressions": 320, "ctr": 0.0344, "position": 6.4 },
{ "date": "2026-03-07", "clicks": 0, "impressions": 0, "ctr": null, "position": null }
]
}
]
}sourcesays how the keyword was added:suggested,manualorimport.intentis an estimate from the query text:informational,commercial,transactional,navigationalorunclassified.groupIdslists the manual groups the keyword is a member of. Rule groups are not listed here; use thegroupparameter to resolve them.- A keyword with no data in the range has zero clicks and impressions and
nullforctrandposition. dailyhas one entry per day the keyword has stored data for, in date order. Days with no impressions are included as zeros.
GET /projects/:id/groups
The keyword groups and folders of a project, in the order of the rank tracker. No parameters.
{
"project": { "id": "Ab12C", "domain": "example.com" },
"groups": [
{ "id": "Fo1dR", "name": "Shoes", "kind": "folder", "parentId": null, "position": 0, "pinned": false, "rule": null, "keywordIds": null },
{ "id": "Gr4Zx", "name": "Running", "kind": "manual", "parentId": "Fo1dR", "position": 1, "pinned": true, "rule": null, "keywordIds": ["k7Qp2", "m3Lw9"] },
{
"id": "Ru1e5",
"name": "Page one, contains trail",
"kind": "rule",
"parentId": "Fo1dR",
"position": 2,
"pinned": false,
"rule": {
"match": "all",
"conditions": [
{ "field": "query", "op": "contains", "value": "trail" },
{ "field": "position", "op": "lte", "value": 10 }
]
},
"keywordIds": null
}
]
}kindisfolder,manualorrule.parentIdis the id of the folder the group sits in, ornull.keywordIdsis filled for manual groups only (archived keywords are left out). For a rule group, call the keywords endpoint withgroupset to its id.ruleis the rule definition of a rule group andnullotherwise.
GET /projects/:id/page-groups
The page groups (content groups) of a project. No parameters. The response holds the definitions, not the member pages: match them against the pages list yourself.
{
"project": { "id": "Ab12C", "domain": "example.com" },
"pageGroups": [
{
"id": "Pg7Ty",
"name": "Blog",
"position": 0,
"rule": { "match": "any", "conditions": [{ "op": "starts_with", "value": "/blog/" }] },
"pinnedUrls": ["/guides/size-chart"]
}
]
}GET /projects/:id/site
Clicks, impressions, CTR and average position of the whole property per day, plus the totals of the range. This covers every query, including the ones Search Console anonymises, and is not limited by plan history.
| Parameter | Type | Default | Allowed values |
|---|---|---|---|
from, to, period | see Date ranges | last 28 days | |
device | string | all | all, desktop, mobile, tablet |
country | string | none | Three-letter country code (ISO 3166-1 alpha-3, for example deu). Cannot be combined with a device other than all. |
{
"project": { "id": "Ab12C", "domain": "example.com" },
"range": { "from": "2026-03-11", "to": "2026-03-12", "device": "all", "country": null, "provisionalAfter": "2026-03-10" },
"totals": { "clicks": 1930, "impressions": 61200, "ctr": 0.0315, "position": 14.8 },
"daily": [
{ "date": "2026-03-11", "clicks": 980, "impressions": 30900, "ctr": 0.0317, "position": 14.6 },
{ "date": "2026-03-12", "clicks": 950, "impressions": 30300, "ctr": 0.0314, "position": 15.0 }
]
}dailystops at the last synced day. A stored day without data is returned as zeros; days that are not synced yet are left out.- With
country,rangealso hascountryMonthlyThrough: the last day that is stored as monthly totals (ornull). Days up to it are the month's total spread evenly over its days, and only the project's top 20 countries have data there.
GET /projects/:id/breakdowns
Totals over a range split by country, device and search appearance, and the brand / non-brand split. Parameters: from, to, period (see Date ranges).
{
"project": { "id": "Ab12C", "domain": "example.com" },
"range": {
"from": "2026-02-13",
"to": "2026-03-12",
"availableFrom": "2024-11-20",
"availableThrough": "2026-03-12",
"provisionalAfter": "2026-03-10",
"countryMonthlyThrough": "2025-12-31"
},
"countries": [
{ "country": "deu", "clicks": 18400, "impressions": 512000, "ctr": 0.0359, "position": 12.1 },
{ "country": "aut", "clicks": 2100, "impressions": 70300, "ctr": 0.0299, "position": 13.4 }
],
"devices": [
{ "device": "mobile", "clicks": 14900, "impressions": 431000, "ctr": 0.0346, "position": 11.8 },
{ "device": "desktop", "clicks": 6800, "impressions": 189000, "ctr": 0.036, "position": 13.9 }
],
"appearances": [
{ "appearance": "PRODUCT_SNIPPETS", "clicks": 3100, "impressions": 98000, "ctr": 0.0316, "position": 9.7 }
],
"brand": {
"brand": { "clicks": 5200, "impressions": 21000, "ctr": 0.2476, "position": 1.4 },
"nonBrand": { "clicks": 12800, "impressions": 455000, "ctr": 0.0281, "position": 13.2 },
"anonymous": { "clicks": 4100, "impressions": 151000 }
}
}availableFromandavailableThroughare the days the breakdowns cover for this project. Both arenullbefore the first collection.- Countries and devices are sorted by clicks, appearances by impressions. Rows without clicks and impressions are left out.
- Search appearances overlap, because one result can carry several. They do not add up to the site total.
branduses the project's brand terms.anonymousis the site total minus brand and non-brand: the queries Search Console hides.- Countries up to
countryMonthlyThroughare monthly totals: the top 20 countries plus a row withcountryset tootherfor the rest. A range that covers part of such a month gets that month spread evenly over its days.
GET /projects/:id/pages
The pages of the property with their totals over a range, most clicks first. This is the same list as the Pages tab of the Traffic screen.
| Parameter | Type | Default | Allowed values |
|---|---|---|---|
from, to, period | see Date ranges | last 28 days | to must be before today (UTC). |
country | string | none | Three-letter country code, for example deu. |
limit | integer | 1000 | 1 to 25000 |
offset | integer | 0 | 0 or more |
{
"project": { "id": "Ab12C", "domain": "example.com" },
"range": { "from": "2026-02-13", "to": "2026-03-12", "country": null },
"source": { "kind": "gsc", "fetchedAt": "2026-03-14T06:02:11.000Z", "truncated": false, "capped": false },
"paging": { "total": 3184, "limit": 2, "offset": 0 },
"pages": [
{ "url": "https://example.com/running-shoes", "clicks": 1420, "impressions": 38900, "ctr": 0.0365, "position": 7.3 },
{ "url": "https://example.com/blog/trail-guide", "clicks": 960, "impressions": 41200, "ctr": 0.0233, "position": 11.9 }
]
}source.kindisgsc(Search Console API) orbigquerywhen the project has a working BigQuery connection.fetchedAtis when the list was fetched from Google.source.truncatedistruewhen Google had more rows than the list holds.- Lists are fetched from Google on demand and then cached, shared with the app. The first request for a range can take up to a minute on a large site; repeat requests are fast.
From the Search Console API a list holds up to 25,000 pages or 50,000 queries per range. With a working BigQuery connection it holds up to 100,000 rows.
GET /projects/:id/queries
Every query of the property with its totals over a range, tracked or not, most clicks first. Parameters, source, paging and limits are the same as for pages.
{
"project": { "id": "Ab12C", "domain": "example.com" },
"range": { "from": "2026-02-13", "to": "2026-03-12", "country": "deu" },
"source": { "kind": "gsc", "fetchedAt": "2026-03-14T06:02:11.000Z", "truncated": false, "capped": false },
"paging": { "total": 18250, "limit": 2, "offset": 0 },
"queries": [
{ "query": "example shoes", "clicks": 2210, "impressions": 6400, "ctr": 0.3453, "position": 1.2, "branded": true, "trackedKeywordId": null },
{ "query": "running shoes", "clicks": 340, "impressions": 9800, "ctr": 0.0347, "position": 6.1, "branded": false, "trackedKeywordId": "k7Qp2" }
]
}brandedistruewhen the query matches the project's brand terms.trackedKeywordIdis the id of the tracked keyword for this query, ornullwhen you do not track it.
GET /projects/:id/alerts
The alerts of a project, newest first: the built-in checks and your own alert rules, with the same title and text as the Alerts feed.
| Parameter | Type | Default | Allowed values |
|---|---|---|---|
status | string | all | open, resolved, all |
since | date YYYY-MM-DD | none | Only alerts created on or after this day (UTC). |
limit | integer | 100 | 1 to 500 |
{
"project": { "id": "Ab12C", "domain": "example.com" },
"alerts": [
{
"id": "7b0c2f8e-4a1d-4c3b-9e52-1f6a8d3b5c70",
"type": "keyword_drop",
"kind": "Rank drop",
"severity": "warning",
"title": "“running shoes” dropped from #4.2 to #9.8",
"body": "Average position, 7 days to Mar 10, compared with the 7 days before (−5.6 positions).",
"date": "2026-03-10",
"keywordId": "k7Qp2",
"url": "https://serplyze.com/Ab12C?kw=k7Qp2",
"createdAt": "2026-03-13T05:14:02.000Z",
"resolvedAt": null
}
]
}typeis the machine name of the check, for examplekeyword_drop,keyword_lost,traffic_drop,rule,url_mismatchorurl_changed.kindis its label in the app.severityisinfo,warningorcritical.dateis the day the alert is about, when the alert names one.keywordIdis set for keyword alerts.urlopens the alert's subject in the app.resolvedAtisnullwhile the alert is open.
GET /projects/:id/notes
The chart notes that apply to a project, oldest first: workspace notes, project notes and notes on a group, a keyword or a page.
| Parameter | Type | Default | Allowed values |
|---|---|---|---|
from | date YYYY-MM-DD | none | Keep notes (and note ranges) that end on or after this day. |
to | date YYYY-MM-DD | none | Keep notes that start on or before this day. from must not be after to. |
clientVisible | boolean | all notes | true returns only notes marked as visible to clients, as reports show them. false returns all notes. |
{
"project": { "id": "Ab12C", "domain": "example.com" },
"notes": [
{
"id": "c1d4e9a2-6b7f-4e0a-8c15-2a9f3b7d6e41",
"scope": "keyword",
"date": "2026-02-20",
"endDate": null,
"category": "content",
"text": "Rewrote the buying guide intro",
"clientVisible": true,
"author": "Sam Example",
"group": null,
"keyword": { "id": "k7Qp2", "query": "running shoes" },
"page": null,
"createdAt": "2026-02-20T10:02:31.000Z",
"updatedAt": "2026-02-20T10:02:31.000Z"
}
]
}scopeisworkspace,project,group,keywordorpage.groupandkeywordare objects with an id and a name or query;pageis the page URL. The ones that do not apply arenull.endDateis the last day of a range note andnullfor a single day.categoryis one oflaunch,content,technical,migration,campaign,other.
What the API does not do
The API is read-only and has no webhooks. It does not add keywords, edit groups, create notes or resolve alerts, and it has no endpoints for reports, backlinks, content findings, forecasts or AI search data. Use exports for those screens.
Read next
- Google SheetsPull keywords, groups, site totals, pages or queries of a project into Google Sheets with a live CSV link and IMPORTDATA.
- BigQueryConnect a Search Console bulk export in BigQuery so the Traffic lists read every query and page from it instead of the API.
- ExportsDownload any table, list or chart as CSV or Excel, copy it for Google Sheets, and save reports as PDF. What each export contains and its limits.
- How the data worksWhere the numbers come from, what average position means, how fresh the data is and how much history you get.