# Serpel REST API reference

URL: https://serpel.app/docs/api

Updated: 2026-10-10

The Serpel REST API under `https://serpel.app/api/v1` offers the same features as the dashboard and the CLI, as JSON over HTTPS. You authenticate with an API token that carries permissions called scopes. This page lists the scopes, errors, rate limits, credit rules and every endpoint, and ends with curl examples.

## What is the base URL of the Serpel API?

All endpoints live under `https://serpel.app/api/v1`. Paths in this reference are relative to it, so `/projects` means `https://serpel.app/api/v1/projects`. Requests and responses use JSON. Send `Content-Type: application/json` with a body. `GET https://serpel.app/api/v1` returns a machine-readable index of the endpoints without authentication, and `GET https://serpel.app/api/health` returns the service status.

## How do you authenticate with the API?

Send an API token as a bearer token. Tokens start with `vsk_`. Create one in the dashboard under Settings, API tokens, with `serpel tokens create` or with `POST /tokens`.

```http
GET /api/v1/projects HTTP/1.1
Host: serpel.app
Authorization: Bearer vsk_your_token
```

- A token can only call endpoints whose scope it has. Without it, the API answers with 403 `forbidden`.
- The project role still applies: viewers read, editors change, owners manage members and delete the project.
- The dashboard uses a session cookie instead of a token. Cookie requests that change data must come from the Serpel origin.
- OAuth access tokens issued to MCP clients only work at the MCP server. The REST API rejects them with 403 `forbidden`.

## Which scopes can an API token have?

Serpel has 18 scopes. Choose the smallest set that your integration needs.

| Scope | Label in the dashboard | What it allows |
| --- | --- | --- |
| `projects:read` | Read projects | Read projects, overviews, members, reports, search data, web analytics, conversions and integrations. |
| `projects:write` | Create and edit projects | Create, change and delete projects and members, connect search data, web analytics and integrations, and manage report recipients and webhooks. |
| `keywords:read` | Read keyword lists | Read keyword lists, their keywords and saved research runs. |
| `keywords:write` | Research keywords and edit lists | Run keyword research (uses credits), create and change keyword lists, import keywords and refresh metrics. |
| `audit:read` | Read crawls and issues | Read crawls, pages, issues, comparisons and Core Web Vitals. |
| `audit:write` | Start crawls | Start crawls. |
| `rankings:read` | Read rankings | Read tracked keywords, rank history, stored search results, competitors, AI prompts and AI visibility. |
| `rankings:write` | Track keywords and start rank checks | Track and remove keywords, start rank checks, AI checks and monitoring runs, and manage competitors and AI prompts. |
| `tasks:read` | Read tasks | Read tasks. |
| `tasks:write` | Create and edit tasks | Create, change and delete tasks. |
| `export:read` | Export data | Export issues, keywords, rankings and tasks. |
| `billing:read` | Read balance and usage | Read balance, usage, the credit ledger, the plan and the agent spending settings, and estimate costs. |
| `billing:write` | Top up balance | Start checkouts, change or cancel the subscription and change the agent spending settings. Workspace owners only. |
| `codebase:read` | Read codebase snapshots | Read codebase snapshots. |
| `codebase:write` | Upload codebase snapshots | Upload codebase snapshots. |
| `recommendations:read` | Read recommendations | Read recommendations. |
| `recommendations:write` | Accept, dismiss and complete recommendations | Accept, dismiss, complete and reopen recommendations. |
| `mcp:read` | Use the MCP server (read-only) | Use the MCP server, read-only. The REST API does not need this scope. |

### Restrict a token to projects

A token can be limited to a list of projects with `projectIds`. Such a token sees only those projects. A project outside the list answers with 404 `not_found`, as if it did not exist, and lists leave it out. Tokens can also expire after 1 to 365 days.

```bash
curl -X POST https://serpel.app/api/v1/tokens \
  -H "Authorization: Bearer $SERPEL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "CI crawl", "scopes": ["projects:read", "audit:read", "audit:write"], "projectIds": ["0b6f0f0e-6d1a-4c8e-9a53-4f9d2d7b8a10"], "expiresInDays": 90}'
```

In the response, `token` holds the metadata and `secret` holds the token itself. The secret is shown exactly once. Creating tokens needs a token with all permissions. A token restricted to projects can only create tokens for its own projects.

## What do responses and pagination look like?

```json
{ "data": { } }

{ "data": [ ], "pagination": { "limit": 50, "offset": 0, "total": 123 } }
```

Single objects come in `data`. Lists add `pagination`. Lists accept `limit` (1 to 200, default 50), `offset` (from 0) and usually `q` to search. Every response carries an `X-Request-ID` header. You can send your own `X-Request-ID` (up to 128 letters, digits, `.`, `_`, `:` and `-`) and quote the value when you contact support.

## How does the API report errors?

```json
{
  "error": {
    "code": "validation_error",
    "message": "Enter a domain.",
    "details": [{ "path": "domain", "message": "Enter a domain." }]
  }
}
```

| HTTP | Code | Meaning |
| --- | --- | --- |
| 400 | `validation_error` | The input is invalid. `details` lists `path` and `message` for each problem. |
| 400 | `bad_request` | The request is malformed, for example invalid JSON or a body over 2.5 MB. |
| 400 | `authorization_pending`, `slow_down`, `expired_token`, `access_denied` | Answers of the device login at `POST /auth/device/token`. |
| 401 | `unauthorized` | The token is missing, invalid, expired or revoked. |
| 402 | `insufficient_credits` | Not enough credits. `details` has `balance`, `available` and `required`. |
| 403 | `forbidden` | A scope or role is missing, an OAuth token was used on the REST API, or the workspace is blocked from paid operations. |
| 403 | `plan_limit` | The plan’s limits are exceeded: projects, tracked keywords, AI prompts or daily monitoring. `details` has `plan` and `limits`. |
| 404 | `not_found` | The item does not exist or the token cannot see it. |
| 409 | `conflict` | The state does not allow it, for example a project that already exists (`details.existingProjectId`). |
| 429 | `rate_limited` | Too many requests. The `Retry-After` header and `details.retryAfterSeconds` tell you how long to wait. |
| 429 | `budget_exceeded` | The workspace’s daily credit limit or a provider budget is reached. |
| 500 | `internal_error` | An error on the server. Retry later and quote the `X-Request-ID`. |
| 502 | `provider_error` | A data provider failed or could not be reached. |
| 503 | `provider_not_configured` | The data provider for this feature is not set up. |

## What are the API rate limits?

| Limit | Applies to | Window |
| --- | --- | --- |
| 600 requests | Each API token, for all endpoints. | One minute |
| 30 operations | Each workspace, for crawls, rank checks, AI checks, monitoring runs, keyword research and list refreshes. | One minute |
| 20 requests | Each IP address, for all free tool endpoints under `/tools/`, which need no token. | 10 minutes |
| 200 requests | Each IP address, for all free tool endpoints under `/tools/`. | One day |

When you exceed a limit, the API answers with HTTP 429 and `rate_limited`. Wait for the seconds in the `Retry-After` header, then continue. The MCP server has its own limit, described in the [Claude Code guide](https://serpel.app/docs/mcp/claude-code).

## How do idempotency keys work?

`POST /keywords/research` and `POST /keyword-lists/{id}/refresh` use credits and answer synchronously. Add an `Idempotency-Key` header with up to 200 letters, digits and the characters `.`, `_`, `:` and `-` to make a retry safe. A repeated request with the same key does not call the data provider again and does not book credits again. Without the header, the server generates a random key, which protects nothing on a retry. The CLI creates a key for you and accepts `--idempotency-key`.

## How do credits work in the API?

Reading data is free. These operations use credits, at the prices of the dashboard:

| Operation | Unit | Credits |
| --- | --- | --- |
| Scheduled monitoring, up to top 50 | per keyword | 2 |
| Scheduled monitoring, top 100 | per keyword | 3 |
| Rank check right now, up to top 50 | per keyword | 5 |
| Rank check right now, top 100 | per keyword | 9 |
| ChatGPT answer check | per question | 2 |
| Google AI Overview check | per question | 2 |
| Keyword research | per search | 8 + 4 per 50 results |
| Keyword research by domain | per domain | 4 + 2 per 50 results |
| Site crawl | per 20 pages | 1 |

Estimate a cost first with `POST /billing/estimate`. It needs `billing:read`, books nothing and returns `minCredits`, `maxCredits` and a `breakdown`. Supported operations are `rankCheck`, `aiCheck`, `keywordResearch`, `keywordListRefresh`, `keywordMetrics`, `crawl` and `monitoring`.

```bash
curl -X POST https://serpel.app/api/v1/billing/estimate \
  -H "Authorization: Bearer $SERPEL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"operation": "rankCheck", "params": {"keywordCount": 20, "depth": 50}}'
```

If the balance is too low for an operation, the API answers with HTTP 402.

```json
{
  "error": {
    "code": "insufficient_credits",
    "message": "The available credits balance isn’t sufficient for this operation.",
    "details": { "balance": 3, "available": 3, "required": 12 }
  }
}
```

## Which endpoints does the REST API have?

The groups below follow the index that `GET /api/v1` returns. Path parameters appear in braces. Methods in one row share the path. The scope line above each table names the scopes the endpoints need.

### Authentication

These endpoints serve the sign-up and sign-in screens of the dashboard and need no token. The Google endpoints are browser redirects, not JSON calls.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/auth/signup` | POST | Create an account with a personal workspace and send a confirmation email |
| `/auth/verify-email` | POST | Verify the email address with a one-time token valid for 24 hours |
| `/auth/password-reset/request` | POST | Request a password reset without revealing whether an account exists |
| `/auth/password-reset/confirm` | POST | Set a new password with a one-time token valid for one hour |
| `/auth/google/start` | GET | Start signing in or signing up with Google as a browser redirect (query intent login, signup or reauthenticate, optional next path); creates a one-time state, PKCE verifier and nonce bound to a short-lived cookie |
| `/auth/google/callback` | GET | Return address of Google: checks state, cookie binding, code and ID token, then signs in, links the account by verified email address or asks for the terms; redirects to the dashboard or back to the sign-in page with an error code |
| `/auth/google/complete` | POST | Create the account after the first Google sign-in once the terms are accepted ({ acceptTerms: true }, cookie from the callback) |
| `/cancellation-requests` | POST | Cancel a subscription without signing in (§ 312k BGB), store the receipt with a timestamp and confirm it by email |

### Free tools

These endpoints need no token. They share one limit per IP address instead: 20 requests per 10 minutes and 200 per day. GET results are cached for a short time: 1 minute for the schema validator and the redirect checker, 5 minutes for the sitemap checker and 10 minutes for the robots.txt check.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/tools/robots-check` | GET | Check without signing in which AI crawlers a domain’s robots.txt allows on the homepage, plus its llms.txt and sitemaps (domain query parameter) |
| `/tools/schema-validate` | GET, POST | Validate JSON-LD, Microdata and RDFa without signing in. GET takes a public page URL (url query parameter) and reads its static HTML. POST takes a JSON body with a code field and checks pasted JSON-LD or HTML up to 512 KB. Returns the detected items by type with properties, errors and warnings with property paths, and Google rich result eligibility |
| `/tools/redirect-check` | GET | Trace the redirect chain of a URL without signing in (url query parameter, optional userAgent: browser, googlebot, googlebotSmartphone, bingbot or gptbot). Returns every hop with status code, Location, response time and server header for up to 10 redirects, loop and downgrade detection, a meta refresh on the final page and recommendations |
| `/tools/sitemap-check` | GET | Validate the XML sitemaps of a domain or a sitemap URL without signing in (url query parameter). Discovers sitemaps through robots.txt and the default locations, checks status, content type, gzip, XML, namespace, limits and entries of up to 10 child sitemaps, and samples the status of up to 20 listed URLs |

### Account

`GET /me` works with any valid token. Changing the profile and using `/tokens` need a token with all permissions. `/account/export`, `/account/delete`, `/account/password/set` and `/account/identities/google` need a dashboard session, not an API token.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/me` | GET, PATCH | Signed-in user and token permissions; change name and email address |
| `/account/export` | GET | Download account data, workspace, projects, integrations and export links as JSON |
| `/account/delete` | POST | Delete the account with session and password, or, without a password, after a confirmation with Google from the last 10 minutes; permanent cleanup after 30 days |
| `/account/security` | GET | Sign-in methods of the account: password set or not, linked Google account, and whether a recent confirmation with Google is valid |
| `/account/password/set` | POST | Set the first password of an account that signed up with Google; needs a confirmation with Google from the last 10 minutes |
| `/account/identities/google` | DELETE | Unlink the Google account; only possible when the account has a password |
| `/tokens` | GET, POST | List and create API tokens |
| `/tokens/{id}` | DELETE | Revoke a token |
| `/providers` | GET | Data providers, daily budget, usage and last use |
| `/favicons/{domain}` | GET | Logo of a domain for the interface, fetched server-side and cached for 7 days |

### Billing

Reading needs `billing:read`. Checkouts and changes need `billing:write` and the workspace owner role.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/billing/summary` | GET | Read balance, daily limit and usage of the last 30 days |
| `/billing/estimate` | POST | Estimate the credit cost of an operation without booking it |
| `/billing/ledger` | GET | Read the entries of the workspace credit ledger |
| `/billing/checkout-sessions` | POST | Create a Stripe Checkout for a credit pack or a custom amount |
| `/billing/subscription` | GET, PATCH | Read plan, limits and subscription or change the plan |
| `/billing/subscription-sessions` | POST | Create a Stripe Checkout for a Starter or Pro subscription |
| `/billing/subscription/cancel` | POST | Cancel the subscription at the end of the billing period |
| `/billing/subscription/resume` | POST | Undo the cancellation of the subscription |
| `/billing/portal-sessions` | POST | Open the Stripe customer portal for payment details and invoices |
| `/billing/agent-spending` | GET, PATCH | Read or change the “Agents can spend credits” setting with daily limit and today’s usage |
| `/billing/stripe-webhook` | POST | Process signed Stripe events idempotently into payments and the credit ledger |

### MCP and connected apps

These endpoints need a dashboard session, or a token with all permissions for the two read and disconnect endpoints.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/oauth/consent` | POST | Decide on an app’s OAuth request (session only, with CSRF token) |
| `/oauth/apps` | GET | Apps connected via OAuth with workspace, permissions and last use |
| `/oauth/apps/{id}` | DELETE | Disconnect a connected app and revoke all its tokens |

### Projects

`projects:read` to read, `projects:write` to create and change. Changing a project needs the editor role, deleting it and managing members the owner role.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/projects` | GET, POST | List and create projects |
| `/projects/{id}` | GET, PATCH, DELETE | Read a project, change settings (including JavaScript rendering, Slack, Discord and custom webhook, channel name, search volume), delete it |
| `/projects/{id}/overview` | GET | Overview with score, rankings, AI visibility and next steps |
| `/projects/{id}/members` | GET, POST | List and add members |
| `/projects/{id}/export` | GET | Export issues, keywords, rankings or tasks as CSV or JSON |

### Audit

`audit:read` to read, `audit:write` to start crawls.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/projects/{id}/crawls` | GET, POST | List and start crawls |
| `/projects/{id}/crawls/compare` | GET | Compare two crawls: new, fixed and changed issues |
| `/projects/{id}/issues` | GET | Issues of the crawl with score and comparison |
| `/projects/{id}/issues/{code}` | GET | Affected URLs of an issue |
| `/crawls/{id}` | GET | Read a crawl, with statistics on rendering, link checks and Core Web Vitals |
| `/crawls/{id}/pages` | GET | Checked URLs of a crawl, with HTML size and rendering flags |
| `/crawls/{id}/pages/{pageId}` | GET | A page with links, issues and signals (rendering, hreflang, structured data, soft 404) |
| `/crawls/{id}/web-vitals` | GET | Core Web Vitals of the crawl from PageSpeed Insights: lab and field values per URL and device |

### Keywords and rankings

`keywords:read` and `keywords:write` for research and lists, `rankings:read` and `rankings:write` for tracked keywords and competitors. Research and list refreshes accept an `Idempotency-Key` header.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/keywords/research` | GET, POST | Start keyword research with idempotency and credits; read the history with cost and duration |
| `/keyword-lists` | GET, POST | Manage keyword lists |
| `/keyword-lists/{id}/refresh` | POST | Refresh list metrics idempotently, billed in credits |
| `/projects/{id}/rankings` | GET, POST | List and add tracked keywords |
| `/projects/{id}/rankings/run` | POST | Start a ranking check right away |
| `/rank-keywords/{id}/history` | GET | Position history of a keyword |
| `/rank-keywords/{id}/serp` | GET | Stored search results of the last check |
| `/locations` | GET | Search locations for local rank tracking |
| `/projects/{id}/competitors` | GET, POST | List and add competitors |
| `/projects/{id}/competitors/overview` | GET | Visibility of you and your competitors |
| `/projects/{id}/competitors/suggestions` | GET | Competitor suggestions from the search results |
| `/projects/{id}/competitors/comparison` | GET | Positions of you and your competitors, keyword by keyword |

### AI visibility and monitoring

`rankings:read` and `rankings:write` for AI visibility and monitoring runs, `projects:read` and `projects:write` for reports, recipients and webhooks.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/projects/{id}/ai-visibility` | GET | Citations in ChatGPT and Google AI Overviews, access for AI crawlers |
| `/projects/{id}/ai-prompts` | GET, POST | List and create prompts for ChatGPT, each with the result of Google AI Overviews |
| `/projects/{id}/ai-prompts/run` | POST | Check prompts right away, also in Google AI Overviews when the switch is on |
| `/projects/{id}/monitoring/run` | POST | Start monitoring with a report right away |
| `/reports` | GET | Reports, optionally only unread ones, with status and duration of the email delivery |
| `/projects/{id}/report-recipients` | GET, POST | Email recipients of the reports and delivery status, with sandbox mode for Amazon SES |
| `/projects/{id}/report-recipients/test` | POST | Send a test email to the recipients |
| `/reports/{id}/email` | POST | Send a report by email again |
| `/reports/{id}/email-preview` | GET | Email of a report as HTML |
| `/projects/{id}/webhook/test` | POST | Send a test message to Slack, Discord and your own URL, or to a single target (channel) |

### Account integrations

`projects:read` to read, `projects:write` to connect, map and disconnect.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/integrations` | GET | Google, Bing, PostHog and Plausible with assignment per project |
| `/integrations/google/authorize` | POST | Authorise Google for Search Console and Analytics |
| `/integrations/{bing\|posthog\|plausible}` | POST, DELETE | Connect with an API key or disconnect |
| `/integrations/{provider}/sync` | POST | Reassign projects |

### Search data

`projects:read` to read, `projects:write` to connect and disconnect.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/projects/{id}/search-console` | GET | Connections to Google Search Console and Bing Webmaster Tools |
| `/projects/{id}/search-console/google/authorize` | POST | Create the authorisation URL for Google |
| `/projects/{id}/search-console/bing` | POST | Connect Bing Webmaster Tools with an API key |
| `/projects/{id}/search-console/{provider}` | PATCH, DELETE | Choose a property or disconnect |
| `/projects/{id}/search-performance` | GET | Clicks, impressions, CTR and position per query or page, from the stored history when it fully covers the period, otherwise live |

### Web analytics

`projects:read` to read, `projects:write` to connect and change.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/projects/{id}/analytics` | GET | Connections to PostHog, Plausible and Google Analytics |
| `/projects/{id}/analytics/{provider}` | POST | Connect PostHog or Plausible with an API key |
| `/projects/{id}/analytics/google_analytics/authorize` | POST | Create the authorisation URL for Google Analytics |
| `/projects/{id}/analytics/{provider}` | PATCH, DELETE | Choose a property or disconnect |
| `/projects/{id}/analytics-report` | GET | Visitors, visits, bounce rate per day, top pages, entry pages, sources and devices |
| `/projects/{id}/analytics/{provider}/conversion-events` | GET, PUT | Read the provider’s events and choose the events that count as a conversion (at most 10) |
| `/projects/{id}/conversions` | GET | Sessions, conversions and conversion rate per entry page from the daily sync |

### Recommendations and codebase

`recommendations:read` and `recommendations:write`, `codebase:read` and `codebase:write`.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/projects/{id}/recommendations` | GET | Recommendations (“opportunities”) with impact, confidence, effort, evidence and data sources, filterable by status and rule |
| `/recommendations/{id}` | GET, PATCH | Read a recommendation, accept it, dismiss it, mark it as done or reopen it |
| `/projects/{id}/codebase-snapshots` | GET, POST | List and upload codebase snapshots (at most 2 MB, 20 per day and project, free of charge) |
| `/codebase-snapshots/{id}` | GET | Snapshot with routes, mapping to live pages, deviations and diff to the previous snapshot |

### Tasks and jobs

`tasks:read` and `tasks:write` for tasks. Jobs follow the scope of their type: `audit:read` for crawls, `rankings:read` for the other jobs.

| Endpoint | Methods | What it does |
| --- | --- | --- |
| `/projects/{id}/tasks` | GET, POST | List and create tasks |
| `/tasks/{id}` | GET, PATCH, DELETE | Read a task, change its status, delete it |
| `/jobs` | GET | Running and finished background jobs |
| `/jobs/{id}/cancel` | POST | Cancel a job |
| `/projects/{id}/activity` | GET | History of a project: jobs, reports by email and keyword research with duration and cost |

### More endpoints

These endpoints exist as well and are used by the CLI, but the index does not list them.

| Endpoint | Methods | What it does | Scope |
| --- | --- | --- | --- |
| `/auth/device` | POST | Start a device login, as `serpel auth login` does | No token |
| `/auth/device/token` | POST | Exchange the device code for a token once the user approved it | No token |
| `/auth/logout` | POST | Revoke the token that sends the request | Any valid token |
| `/jobs/{id}` | GET | Read one job with status, progress, result and last error | `audit:read` for crawls, `rankings:read` for the other jobs |
| `/jobs/{id}/retry` | POST | Restart a failed or cancelled job | `audit:write` for crawls, `rankings:write` for the other jobs |
| `/projects/{id}/rankings/summary` | GET | Top 3, top 10, average position and changes of all tracked keywords | `rankings:read` |
| `/rank-keywords/{id}` | DELETE | Remove a tracked keyword with its history | `rankings:write` |
| `/projects/{id}/competitors/{competitorId}` | DELETE | Remove a competitor | `rankings:write` |
| `/ai-prompts/{id}/history` | GET | Check history of a prompt for ChatGPT and Google AI Overviews | `rankings:read` |
| `/ai-prompts/{id}` | DELETE | Remove a prompt with all its results | `rankings:write` |
| `/keywords/research/{runId}` | GET | Read a saved research run without querying the provider | `keywords:read` |
| `/keyword-lists/{id}` | GET, PATCH, DELETE | Read, rename or delete a keyword list | `keywords:read` to read, `keywords:write` to change |
| `/keyword-lists/{id}/items` | GET, POST | List the keywords of a list or add keywords | `keywords:read` to read, `keywords:write` to add |
| `/keyword-lists/{id}/items/{itemId}` | DELETE | Remove a keyword from a list | `keywords:write` |
| `/keyword-lists/{id}/import` | POST | Import keywords from CSV text | `keywords:write` |
| `/keyword-lists/{id}/export` | GET | Export a keyword list as a CSV or JSON file | `export:read` |
| `/projects/{id}/members/{userId}` | DELETE | Remove a member from a project | `projects:write`, owner role |
| `/projects/{id}/report-recipients/{recipientId}` | DELETE | Remove an email recipient of the reports | `projects:write` |
| `/reports/{id}` | GET | Read one report | `projects:read` |
| `/reports/{id}/read` | POST | Mark a report as read | `projects:read` |
| `/reports/read-all` | POST | Mark all reports, or those of one project, as read | `projects:read` |
| `/reports/unread-count` | GET | Number of unread reports | `projects:read` |
| `/projects/{id}/favicon` | GET | Website icon of a project, fetched server-side | `projects:read` |

## What do complete API examples look like?

The examples assume that `SERPEL_TOKEN` holds a token with the scopes named in each step. The responses are abridged to the fields you need next, and their values are illustrative. The real responses contain more fields.

### Create a project

Needs `projects:write`. The API returns HTTP 201. If you already have a project for the domain, it answers with 409 `conflict` and the ID of the existing project in `details.existingProjectId`.

```bash
curl -X POST https://serpel.app/api/v1/projects \
  -H "Authorization: Bearer $SERPEL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com", "name": "Example", "country": "US", "language": "en"}'
```

```json
{
  "data": {
    "id": "0b6f0f0e-6d1a-4c8e-9a53-4f9d2d7b8a10",
    "name": "Example",
    "domain": "example.com",
    "country": "US",
    "language": "en",
    "role": "owner"
  }
}
```

### Start a crawl and read the issues

Needs `audit:write` to start and `audit:read` to follow the job. The crawl runs in a worker, so the API answers with 202 and a job. If a crawl is already running, you get that crawl back with HTTP 200 and `alreadyRunning: true`. Poll `GET /jobs/{id}` until `status` is `succeeded`, `failed` or `cancelled`, then read the issues. `maxPages` can be 1 to 1000.

```bash
curl -X POST https://serpel.app/api/v1/projects/$PROJECT_ID/crawls \
  -H "Authorization: Bearer $SERPEL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"maxPages": 200}'
```

```json
{
  "data": {
    "crawl": {
      "id": "7d2a4c1e-3b9f-4e0a-8c55-2f61b0a9d3e4",
      "status": "queued",
      "maxPages": 200,
      "pagesCrawled": 0
    },
    "job": {
      "id": "c41e8f27-5a6d-4b13-9d70-e8a2f4b6c915",
      "type": "crawl",
      "status": "queued"
    },
    "alreadyRunning": false
  }
}
```

```bash
curl https://serpel.app/api/v1/jobs/$JOB_ID \
  -H "Authorization: Bearer $SERPEL_TOKEN"

curl https://serpel.app/api/v1/projects/$PROJECT_ID/issues \
  -H "Authorization: Bearer $SERPEL_TOKEN"
```

### Research keywords

Needs `keywords:write` and uses credits. A keyword research costs 8 credits plus 4 credits per block of 50 results, so 20 results cost 12 credits. The `Idempotency-Key` makes a retry safe. `credits.charged` and `credits.balance` show what the call cost and what is left.

```bash
curl -X POST https://serpel.app/api/v1/keywords/research \
  -H "Authorization: Bearer $SERPEL_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: research-cold-brew-2026-10-10" \
  -d '{"mode": "keyword", "keyword": "cold brew concentrate", "country": "US", "language": "en", "limit": 20}'
```

```json
{
  "data": {
    "runId": "5e9b7a30-1c4d-4f82-b6a1-0d3c8e2f7a46",
    "mode": "keyword",
    "query": "cold brew concentrate",
    "country": "US",
    "language": "en",
    "cached": false,
    "items": [
      {
        "keyword": "cold brew concentrate",
        "relation": "seed",
        "searchVolume": 2900,
        "cpc": 1.45,
        "keywordDifficulty": 38,
        "intent": "commercial"
      }
    ],
    "credits": {
      "charged": 12,
      "balance": 488
    }
  }
}
```

### Read rankings

Needs `rankings:read`. Each tracked keyword has its `latest` check and a `change` against the previous one; a positive change means the keyword moved up. A `position` of `null` with status `ok` means the page was not found within the checked depth.

```bash
curl "https://serpel.app/api/v1/projects/$PROJECT_ID/rankings?limit=2&q=cold%20brew" \
  -H "Authorization: Bearer $SERPEL_TOKEN"
```

```json
{
  "data": [
    {
      "id": "a3f1c9d2-6b8e-4d57-9c20-7e4b1a5d8f03",
      "keyword": "cold brew concentrate",
      "country": "US",
      "language": "en",
      "device": "desktop",
      "searchVolume": 2900,
      "latest": {
        "checkedAt": "2026-10-09T06:00:12.000Z",
        "depth": 50,
        "position": 11,
        "url": "https://example.com/cold-brew-concentrate",
        "status": "ok"
      },
      "change": 3
    }
  ],
  "pagination": { "limit": 2, "offset": 0, "total": 1 }
}
```

> **Prefer the CLI for scripts that wait on jobs:** The [CLI](https://serpel.app/docs/cli) wraps these calls, polls jobs with `--wait` and maps errors to exit codes, so you write less glue code.