---
title: "Serql REST API"
description: "Reference for the Serql REST API: base URL, authentication, endpoints, errors, rate limits, and the OpenAPI document."
canonical: https://www.serql.io/docs/api
markdown: https://www.serql.io/docs/api.md
last-updated: 2026-09-14
section: api
site: "Serql"
llms: https://www.serql.io/llms.txt
---

# Serql REST API

- **Base URL:** `https://app.serql.io/api`
- **OpenAPI 3.1:** https://www.serql.io/openapi.json (also linked from the [API catalog](https://www.serql.io/.well-known/api-catalog) and advertised in `Link` headers)
- **Authentication:** `Authorization: Bearer <Clerk OAuth access token>` or the web app's session cookie. See https://www.serql.io/auth.md.
- **Format:** JSON request and response bodies; dates are `YYYY-MM-DD`; identifiers are UUIDs.
- **Data source:** Google Search Console Search Analytics, read-only, fetched on demand and cached for up to six hours per query; never stored in the database. Data typically lags two to three days.

## Conventions

- Every property-scoped route lives under `/api/properties/{id}/…` and is authorized per user: a property that is not visible to the caller returns `404`, never `403`.
- List endpoints accept a date window (`startDate`, `endDate`) and optional comparison window (`compareStart`, `compareEnd`).
- `classification` narrows results to `branded`, `nonBranded`, `segment:<uuid>`, or `topicCluster:<uuid>`.
- `searchType` selects `web` (default), `discover`, `googleNews`, `image`, or `video`.
- `filters` is a JSON-encoded array (max 50) of `{ dimension, operator, value, logic? }` conditions over `query`, `page`, `country`, `device`, and `searchAppearance` using `contains`, `not_contains`, `equals`, `matches_regex`, or `or_list` (comma-separated values); `logic` is `and` (default) or `or`.
- Breakdown tables from `scope=tables` return 500 rows by default; raise `queryLimit` and `pageLimit` (up to 5,000, our fetch ceiling) for more. `meta.coverage` reports rows shown against rows known (`totalRowsCapped` means "5,000+") and the share of the property's clicks they cover. Query rows never sum to the property total because Google anonymises some queries.
- Errors are JSON: `{ "error": string, "code"?: string, "hint"?: string, "correlationId"?: string }`. Stack traces are never returned.
- Rate limits are per user; responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` on `429`.

## Versioning and deprecation

- Every response carries `X-API-Version` (a date, currently `2026-09-14`). The base path `/api` is stable and is not versioned in the URL.
- Additive changes (new fields, new endpoints, new optional parameters) ship without a version change.
- Breaking changes ship under a new version date. The old behaviour keeps working for at least **90 days**, during which affected responses carry `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers and a `Link: <changelog>; rel="deprecation"` pointing at https://www.serql.io/changelog.
- Clients may pin a version by sending `X-API-Version` on requests; a value the deployment does not support returns `400` with `code: "unsupported_api_version"`. Pinning is optional.

## Idempotent writes

Agents retry on network failures, so the endpoints that create records (`POST` on annotations, segments, topic clusters, share links, tags, team members, filter presets, and branded keywords) honour an `Idempotency-Key` header (1-255 printable ASCII characters, unique per operation, for example a UUID). The OpenAPI document marks each write operation with `x-idempotency: enforced` or `ignored`; on ignored operations the header is accepted but has no effect, so keep client-side deduplication for those.

- A malformed key returns `400` with `code: "invalid_idempotency_key"`.
- The first request with a key runs normally; its response is stored for 24 hours under (user, method, path, key) and carries `Idempotency-Replayed: false`.
- A retry with the same key **and the same body** returns the stored status and body with `Idempotency-Replayed: true`.
- The same key with a different body returns `422` with `code: "idempotency_key_reused"`.
- A retry while the first request is still running returns `409` with `code: "idempotency_in_progress"` and `Retry-After`.
- Responses with status `5xx` or `429` are not stored, so they can be retried with the same key. Bodies over 256 KB are returned but not stored.
- Deployments without Redis cannot store replays; they process the request normally and add `Idempotency-Storage: unavailable`.

## Endpoints

### Discovery

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api` | `getApiIndex` — API index | session-or-oauth |

### System

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api/health` | `getHealth` — Liveness and dependency health | public |
| `GET` | `/api/accounts/health` | `listGoogleAccountHealth` — Google connection health | session-or-oauth |
| `GET` | `/api/feature-flags` | `getFeatureFlags` — Client-visible feature flags | session-or-oauth |

### Billing

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api/billing/plans` | `listBillingPlans` — Public plan catalogue | public |
| `GET` | `/api/billing/summary` | `getBillingSummary` — Subscription and usage summary | session-or-oauth |

### Properties

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api/properties` | `listProperties` — List properties with headline metrics | session-or-oauth |
| `POST` | `/api/properties/sync` | `syncProperties` — Discover properties from Google | session-or-oauth |
| `GET` | `/api/properties/tracking` | `getPropertyTracking` — List owned properties and tracking usage | session-or-oauth |
| `PUT` | `/api/properties/tracking` | `replacePropertyTracking` — Replace the tracked-property selection | session-or-oauth |
| `PUT` | `/api/properties/{id}/favorite` | `setPropertyFavorite` — Favorite or unfavorite a property | session-or-oauth |
| `PUT` | `/api/properties/{id}/hidden` | `setPropertyHidden` — Hide or unhide a property | session-or-oauth |

### Analytics

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api/properties/{id}/performance` | `getPropertyPerformance` — Performance time series and breakdown tables | session-or-oauth |
| `GET` | `/api/properties/{id}/cannibalization` | `getPropertyCannibalization` — Find pages competing for the same query | session-or-oauth |
| `GET` | `/api/properties/{id}/growth-opportunities` | `getPropertyGrowthOpportunities` — Striking-distance keyword opportunities | session-or-oauth |
| `GET` | `/api/properties/{id}/query-insights` | `getPropertyQueryInsights` — Query visibility and branded traffic split | session-or-oauth |
| `GET` | `/api/properties/{id}/query-count` | `getPropertyQueryCount` — Daily unique-query counts (legacy) | session-or-oauth |
| `GET` | `/api/properties/{id}/query-count-raw` | `exportPropertyQueryCountRaw` — Export the raw rows behind the query-count chart (CSV) | session-or-oauth |
| `GET` | `/api/properties/{id}/countries` | `listPropertyCountries` — Country breakdown | session-or-oauth |
| `GET` | `/api/properties/{id}/countries-timeseries` | `getPropertyCountriesTimeseries` — Country trends over time | session-or-oauth |
| `GET` | `/api/properties/{id}/table-sparklines` | `getPropertyTableSparklines` — Per-row sparkline series for the breakdown tables | session-or-oauth |
| `GET` | `/api/properties/{id}/export` | `exportPropertyPerformanceCsv` — Export a breakdown table as CSV | session-or-oauth |
| `GET` | `/api/properties/{id}/branded-traffic` | `getPropertyBrandedTraffic` — Branded versus non-branded traffic (legacy) | session-or-oauth |

### Classifications

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api/properties/{id}/branded-keywords` | `listPropertyBrandedKeywords` — List branded keywords | session-or-oauth |
| `POST` | `/api/properties/{id}/branded-keywords` | `createPropertyBrandedKeywords` — Add branded keywords | session-or-oauth |
| `DELETE` | `/api/properties/{id}/branded-keywords` | `deletePropertyBrandedKeyword` — Remove a branded keyword | session-or-oauth |
| `GET` | `/api/properties/{id}/segments` | `listPropertySegments` — List saved page classifications | session-or-oauth |
| `POST` | `/api/properties/{id}/segments` | `createPropertySegment` — Create a segment | session-or-oauth |
| `PUT` | `/api/properties/{id}/segments/{segmentId}` | `updatePropertySegment` — Replace a segment | session-or-oauth |
| `DELETE` | `/api/properties/{id}/segments/{segmentId}` | `deletePropertySegment` — Delete a segment | session-or-oauth |
| `GET` | `/api/properties/{id}/topic-clusters` | `listPropertyTopicClusters` — List saved query classifications | session-or-oauth |
| `POST` | `/api/properties/{id}/topic-clusters` | `createPropertyTopicCluster` — Create a topic cluster | session-or-oauth |
| `PUT` | `/api/properties/{id}/topic-clusters/{clusterId}` | `updatePropertyTopicCluster` — Replace a topic cluster | session-or-oauth |
| `DELETE` | `/api/properties/{id}/topic-clusters/{clusterId}` | `deletePropertyTopicCluster` — Delete a topic cluster | session-or-oauth |

### Annotations

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api/properties/{id}/annotations` | `listPropertyAnnotations` — List annotations | session-or-oauth |
| `POST` | `/api/properties/{id}/annotations` | `createPropertyAnnotation` — Create an annotation | session-or-oauth |
| `PUT` | `/api/properties/{id}/annotations/{annotationId}` | `updatePropertyAnnotation` — Update an annotation | session-or-oauth |
| `DELETE` | `/api/properties/{id}/annotations/{annotationId}` | `deletePropertyAnnotation` — Delete an annotation | session-or-oauth |

### Tags

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `POST` | `/api/properties/{id}/tags` | `replacePropertyTags` — Replace a property's tags | session-or-oauth |
| `GET` | `/api/tags` | `listTags` — List tags | session-or-oauth |
| `POST` | `/api/tags` | `createTag` — Create a tag | session-or-oauth |
| `PUT` | `/api/tags/{id}` | `updateTag` — Rename or reorder a tag | session-or-oauth |
| `DELETE` | `/api/tags/{id}` | `deleteTag` — Delete a tag | session-or-oauth |
| `GET` | `/api/tags/{id}/properties` | `listTagProperties` — List the properties carrying a tag | session-or-oauth |
| `POST` | `/api/tags/{id}/properties` | `replaceTagProperties` — Replace the properties carrying a tag | session-or-oauth |

### Sharing

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api/properties/{id}/share-links` | `listPropertyShareLinks` — List active share links | session-or-oauth |
| `POST` | `/api/properties/{id}/share-links` | `createPropertyShareLink` — Create a share link | session-or-oauth |
| `DELETE` | `/api/properties/{id}/share-links/{linkId}` | `revokePropertyShareLink` — Revoke a share link | session-or-oauth |
| `GET` | `/api/share/{token}/performance` | `getSharedPerformance` — Read a shared dashboard | share-token |

### Team

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api/team` | `listTeamMembers` — List team grants | session-or-oauth |
| `POST` | `/api/team` | `inviteTeamMember` — Grant a user access to properties | session-or-oauth |
| `PUT` | `/api/team/{memberId}` | `updateTeamMemberRole` — Change a grant's role | session-or-oauth |
| `DELETE` | `/api/team/{memberId}` | `removeTeamMember` — Revoke a grant | session-or-oauth |

### Filters

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api/filter-presets` | `listFilterPresets` — List saved filter presets | session-or-oauth |
| `POST` | `/api/filter-presets` | `createFilterPreset` — Create a filter preset | session-or-oauth |
| `PUT` | `/api/filter-presets/{id}` | `updateFilterPreset` — Update a filter preset | session-or-oauth |
| `DELETE` | `/api/filter-presets/{id}` | `deleteFilterPreset` — Delete a filter preset | session-or-oauth |

### Integrations

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api/integrations/status` | `getIntegrationsStatus` — Google and SEO provider connection status | session-or-oauth |
| `GET` | `/api/integrations/ahrefs` | `getAhrefsIntegration` — Ahrefs connection status | session-or-oauth |
| `PUT` | `/api/integrations/ahrefs` | `connectAhrefsIntegration` — Store Ahrefs credentials | session-or-oauth |
| `DELETE` | `/api/integrations/ahrefs` | `disconnectAhrefsIntegration` — Delete Ahrefs credentials | session-or-oauth |
| `POST` | `/api/integrations/ahrefs/test` | `testAhrefsIntegration` — Test Ahrefs credentials | session-or-oauth |
| `GET` | `/api/integrations/dataforseo` | `getDataForSeoIntegration` — DataForSEO connection status | session-or-oauth |
| `PUT` | `/api/integrations/dataforseo` | `connectDataForSeoIntegration` — Store DataForSEO credentials | session-or-oauth |
| `DELETE` | `/api/integrations/dataforseo` | `disconnectDataForSeoIntegration` — Delete DataForSEO credentials | session-or-oauth |
| `POST` | `/api/integrations/dataforseo/test` | `testDataForSeoIntegration` — Test DataForSEO credentials | session-or-oauth |
| `GET` | `/api/properties/{id}/keyword-research` | `getPropertyKeywordResearch` — Keyword ideas from a connected SEO provider | session-or-oauth |
| `GET` | `/api/properties/{id}/backlinks` | `getPropertyBacklinks` — Backlink profile from a connected SEO provider | session-or-oauth |

### Privacy

| Method | Path | Operation | Access |
| --- | --- | --- | --- |
| `GET` | `/api/privacy/access` | `exportPersonalData` — Export personal data | session-or-oauth |
| `POST` | `/api/privacy/delete` | `deletePersonalData` — Delete personal data | session-or-oauth |

## Example

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://app.serql.io/api/properties/PROPERTY_ID/performance?startDate=2026-08-01&endDate=2026-08-28&scope=tables&classification=nonBranded"
```

## Sandbox and testing

There is no separate sandbox host: the API is read-only against Google Search Console, so exploratory calls cannot damage search data. To test safely, connect a Google account that only has access to a test property, or use a self-hosted instance (https://www.serql.io/docs/self-hosting) pointed at your own Google Cloud project. Write endpoints (annotations, segments, tags, share links, team) only touch Serql's own records and can be deleted through the same API.

## Generating a client

The OpenAPI document is complete enough for code generation, for example:

```bash
npx @openapitools/openapi-generator-cli generate -i https://www.serql.io/openapi.json -g typescript-fetch -o ./serql-client
```
