Start a crawl
POST/crawls
- Scope:
- crawl
- Rate class:
- start
- Operation id:
- start_crawl
Starts a crawl of one of your sites (My Sites) and returns at once with 202 Accepted, the crawl object and a Location header; follow it with GET /crawls/{id}. It goes through the same checks as the dashboard (public addresses only, blocked domains, your plan's rates, the owned-site rate for a verified site).
Credits. Credits are checked before the crawl exists: the most it can cost (max_pages times the page rate) must fit what the workspace has left this month, less what your other running API crawls may still spend (402 insufficient_credits otherwise; credits.available is already net of those holds). You are charged when the crawl ends, for the pages it checked.
Limits. A key's daily_credit_cap bounds the worst-case cost of the crawls it starts per UTC day (429 daily_credit_cap_reached); a key made without one may crawl at most 500 pages. Crawls of one site through the API must be 10 minutes apart (429 crawl_too_soon; a crawl cancelled before it started does not count). A key may start 10 crawls a day and the workspace 20; 2 may run at once. A site that already has a pending or running crawl answers 409 crawl_in_progress with that crawl's id.
Idempotency. Send Idempotency-Key (1 to 255 characters) to make a retry safe: the same key with the same body returns the crawl the first request started (200 and Idempotent-Replayed: true); the same key with another body is 422 idempotency_key_reused; a request still being made answers 409 idempotency_in_progress. Keys are remembered for 24 hours. Use something unique per intended crawl, for example a commit hash.
Needs the crawl scope. Until starting crawls through the API is switched on, this route answers 404.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Key | header | stringoptional | Makes a retry of this request safe: the same key and body return the crawl already started.Range: at most 255 characters. |
Request body
application/json, required.
| Field | Type | Description |
|---|---|---|
| max_pages | integeroptional | Pages to check at most, 1 to 10,000. A key made without a daily credit cap (before the REST API) may ask for at most 500. The most the crawl can cost is max_pages times the page rate, and that must fit the workspace's credits and the key's daily credit cap.Range: 1 to 10000. Default: 100. |
| mode | stringoptional | fast: links only, the cheapest. full: links plus SEO data, structured data and AI readiness for every page (more credits per page).One of: fast, full. Default: fast. |
| preset_id | integer or nulloptional | A scan preset (a PerfBee preset or one of your workspace's): its checks, exclusions, depth and rules apply. mode, max_pages and render_js always come from this request.Range: at least 1. |
| render_js | booleanoptional | Render pages that need JavaScript (smart rendering): rendered pages cost more.Default: false. |
| site_id | integer or nulloptional | A site of your workspace (My Sites): its id from the sites list.Range: at least 1. |
| url | string or nulloptional | A site in My Sites, as an http:// or https:// URL. A URL with a path inside a registered site crawls that folder only. A URL that is not in My Sites is refused: add the site in PerfBee first.Range: at most 2000 characters. |
{
"max_pages": 500,
"mode": "fast",
"render_js": false,
"site_id": 12
}Answer 200
A retry of a start the Idempotency-Key had already made: the same crawl, nothing new was started. Headers: Idempotent-Replayed, Location.
| Field | Type | Description |
|---|---|---|
| id | string | The crawl's id (a UUID). |
| site_id | integer or null | The most specific site of yours this crawl belongs to; null when the site was removed. |
| url | string | The address the crawl started from. |
| mode | string | fast (links and status only) or full (SEO data and AI readiness too). |
| status | string | pending, running, completed, failed, cancelled or partial. New values may appear. |
| started_via | string | What started the crawl.One of: dashboard, schedule, mcp, api. |
| created_at | string or null | When the crawl was queued. |
| started_at | string or null | When the crawler started; null while queued. |
| completed_at | string or null | When it ended; null while pending or running. |
| duration_seconds | number or null | How long it ran, once it ended. |
| max_pages | integer or null | The page limit the crawl was started with; null = no limit. |
| pages_checked | integer | Pages read (final once the crawl ended; see progress while it runs). |
| links_checked | integer | URLs checked, links and files included. |
| broken_count | integer | Broken URLs found. |
| working_count | integer | URLs that worked. |
| results | object or null | The result buckets stored when the crawl ended; null while it runs and for crawls from before 2026-10-20. |
| results.total | integer | Checked URLs: ok + redirects + broken + not_checked. |
| results.ok | integer | URLs that answered normally. |
| results.redirects | integer | URLs that redirected. |
| results.broken | integer | URLs that are broken (404, 5xx, no answer...). |
| results.not_checked | integer | URLs the crawler could not check (a bot wall, robots.txt, a timeout...); never billed, never broken. |
| close_reason | string or null | Why the crawler stopped: finished, closespider_pagecount, blocked, rate_limited, credit_limit... New values may appear. |
| crawl_scope | object or null | What the crawl covered (checks on or off, sitemap, render mode, folder, list mode...). Keys may be added. |
| error | string or null | Why a crawl that stopped early stopped, in a sentence; null otherwise. |
| credits_charged | number or null | Credits this crawl cost so far; null when nothing was charged (queued, free, or nothing checked). |
| progress | object or null | Live counters while the crawl is pending or running; null otherwise. |
| progress.pages_checked | integer | Pages read so far. |
| progress.links_checked | integer | Links and files checked so far. |
| progress.broken_count | integer | Broken URLs found so far. |
| progress.max_pages | integer or null | The page limit of the crawl; null = no limit. |
| progress.updated_at | string or null | When the crawler last reported (a heartbeat each minute). |
| processing | object | Where the crawl's data stands after the crawl itself. A crawl is completed before its pages and fixes are ready. |
| processing.inventory | string | Site Pages (GET /sites/{site_id}/pages) hold this crawl's data. pending = on its way, failed = the sync failed (it is retried for 48 hours), not_applicable = no site of yours takes this crawl in.One of: pending, done, failed, not_applicable. |
| processing.fixes | string | The fix list was refreshed from this crawl. skipped = the inventory sync failed, so no refresh ran; not_applicable = Fixes are off or no site takes the crawl in. The scan_data_ready webhook is the exact moment.One of: pending, done, skipped, not_applicable. |
| report_url | string | The same crawl in the dashboard. |
| render_js | boolean | True when pages are rendered with JavaScript. |
| credits | object or null | The credit estimate made when this request started the crawl; null on a replay. |
| credits.per_page | number | Credits per page at the rate this crawl is billed (owned-site and render rates included). |
| credits.max_cost | number | The most the crawl can cost: max_pages times per_page. It is charged when the crawl ends, for the pages actually checked. |
| credits.remaining_before | number or null | Credits the workspace had left this month before this crawl; null = unlimited. |
| credits.held_by_running_crawls | number | Credits the workspace's other running API crawls may still spend. |
{
"id": "3f2b8c1e-5d47-4a0b-9a6e-1c7d2e8f4b90",
"site_id": 12,
"url": "https://www.example.com",
"mode": "fast",
"status": "pending",
"started_via": "api",
"created_at": "2026-10-31T09:12:44Z",
"max_pages": 500,
"pages_checked": 0,
"links_checked": 0,
"broken_count": 0,
"working_count": 0,
"processing": {
"inventory": "pending",
"fixes": "pending"
},
"report_url": "https://dashboard.perfbee.com/#/brokenLinks?scan=3f2b8c1e-5d47-4a0b-9a6e-1c7d2e8f4b90",
"render_js": false,
"credits": {
"per_page": 0.5,
"max_cost": 250,
"remaining_before": 8759.5,
"held_by_running_crawls": 0
}
}Answer 202
The crawl was queued. Location is its URL; Retry-After is when to look at it first. Headers: Location, Retry-After.
| Field | Type | Description |
|---|---|---|
| id | string | The crawl's id (a UUID). |
| site_id | integer or null | The most specific site of yours this crawl belongs to; null when the site was removed. |
| url | string | The address the crawl started from. |
| mode | string | fast (links and status only) or full (SEO data and AI readiness too). |
| status | string | pending, running, completed, failed, cancelled or partial. New values may appear. |
| started_via | string | What started the crawl.One of: dashboard, schedule, mcp, api. |
| created_at | string or null | When the crawl was queued. |
| started_at | string or null | When the crawler started; null while queued. |
| completed_at | string or null | When it ended; null while pending or running. |
| duration_seconds | number or null | How long it ran, once it ended. |
| max_pages | integer or null | The page limit the crawl was started with; null = no limit. |
| pages_checked | integer | Pages read (final once the crawl ended; see progress while it runs). |
| links_checked | integer | URLs checked, links and files included. |
| broken_count | integer | Broken URLs found. |
| working_count | integer | URLs that worked. |
| results | object or null | The result buckets stored when the crawl ended; null while it runs and for crawls from before 2026-10-20. |
| results.total | integer | Checked URLs: ok + redirects + broken + not_checked. |
| results.ok | integer | URLs that answered normally. |
| results.redirects | integer | URLs that redirected. |
| results.broken | integer | URLs that are broken (404, 5xx, no answer...). |
| results.not_checked | integer | URLs the crawler could not check (a bot wall, robots.txt, a timeout...); never billed, never broken. |
| close_reason | string or null | Why the crawler stopped: finished, closespider_pagecount, blocked, rate_limited, credit_limit... New values may appear. |
| crawl_scope | object or null | What the crawl covered (checks on or off, sitemap, render mode, folder, list mode...). Keys may be added. |
| error | string or null | Why a crawl that stopped early stopped, in a sentence; null otherwise. |
| credits_charged | number or null | Credits this crawl cost so far; null when nothing was charged (queued, free, or nothing checked). |
| progress | object or null | Live counters while the crawl is pending or running; null otherwise. |
| progress.pages_checked | integer | Pages read so far. |
| progress.links_checked | integer | Links and files checked so far. |
| progress.broken_count | integer | Broken URLs found so far. |
| progress.max_pages | integer or null | The page limit of the crawl; null = no limit. |
| progress.updated_at | string or null | When the crawler last reported (a heartbeat each minute). |
| processing | object | Where the crawl's data stands after the crawl itself. A crawl is completed before its pages and fixes are ready. |
| processing.inventory | string | Site Pages (GET /sites/{site_id}/pages) hold this crawl's data. pending = on its way, failed = the sync failed (it is retried for 48 hours), not_applicable = no site of yours takes this crawl in.One of: pending, done, failed, not_applicable. |
| processing.fixes | string | The fix list was refreshed from this crawl. skipped = the inventory sync failed, so no refresh ran; not_applicable = Fixes are off or no site takes the crawl in. The scan_data_ready webhook is the exact moment.One of: pending, done, skipped, not_applicable. |
| report_url | string | The same crawl in the dashboard. |
| render_js | boolean | True when pages are rendered with JavaScript. |
| credits | object or null | The credit estimate made when this request started the crawl; null on a replay. |
| credits.per_page | number | Credits per page at the rate this crawl is billed (owned-site and render rates included). |
| credits.max_cost | number | The most the crawl can cost: max_pages times per_page. It is charged when the crawl ends, for the pages actually checked. |
| credits.remaining_before | number or null | Credits the workspace had left this month before this crawl; null = unlimited. |
| credits.held_by_running_crawls | number | Credits the workspace's other running API crawls may still spend. |
{
"id": "3f2b8c1e-5d47-4a0b-9a6e-1c7d2e8f4b90",
"site_id": 12,
"url": "https://www.example.com",
"mode": "fast",
"status": "pending",
"started_via": "api",
"created_at": "2026-10-31T09:12:44Z",
"max_pages": 500,
"pages_checked": 0,
"links_checked": 0,
"broken_count": 0,
"working_count": 0,
"processing": {
"inventory": "pending",
"fixes": "pending"
},
"report_url": "https://dashboard.perfbee.com/#/brokenLinks?scan=3f2b8c1e-5d47-4a0b-9a6e-1c7d2e8f4b90",
"render_js": false,
"credits": {
"per_page": 0.5,
"max_cost": 250,
"remaining_before": 8759.5,
"held_by_running_crawls": 0
}
}Problems this call documents
- 402 Not enough credits:
insufficient_credits - 404 Not found:
not_found - 409 Crawl in progress:
crawl_in_progress - 409 Request in progress:
idempotency_in_progress - 422 Idempotency key reused:
idempotency_key_reused - 422 Validation failed:
validation_error - 429 Daily crawl limit reached:
crawl_limit_reached - 429 Crawl too soon:
crawl_too_soon - 429 Daily credit cap reached:
daily_credit_cap_reached - 429 Too many running crawls:
too_many_running_crawls - 503 Service busy:
capacity_busy
Cancel a crawl
POST/crawls/{crawl_id}/cancel
- Scope:
- crawl
- Rate class:
- light
- Operation id:
- cancel_crawl
Asks for one of your workspace's crawls to stop. Only a crawl that is pending or running can be cancelled (409 crawl_not_active otherwise). A crawl that is queued, or waiting between two parts, ends at once; a running crawl is told to stop and the crawler does so within a few seconds, so the answer is 202 Accepted and the crawl's status becomes cancelled shortly after: watch GET /crawls/{id}.
Pages checked so far stay billed. The crawl is charged for the pages it checked before it stopped, as after a cancel in the dashboard; a crawl cancelled before it started costs nothing. Cancelling does not give credits back.
Needs the crawl scope (a viewer's key cannot). Until starting crawls through the API is switched on, this route answers 404.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| crawl_id | path | stringrequired | The crawl's id.Range: at most 64 characters. |
Answer 202
Accepted. The body is a CrawlCancelRequested.
| Field | Type | Description |
|---|---|---|
| id | string | The crawl's id. |
| status | string | cancelled when the crawl ended at once (it was queued or waiting for its next part); pending or running while the crawler is stopping it. |
| cancelled | boolean | True when the crawl is already cancelled; false when the crawler has been told and stops within a few seconds. |
| message | string | What happens next, in a sentence. |
{
"id": "3f2b8c1e-5d47-4a0b-9a6e-1c7d2e8f4b90",
"status": "running",
"cancelled": false,
"message": "The crawler was told to stop. It saves the pages it checked, which stay billed. Poll the crawl until its status is cancelled."
}Problems this call documents
- 404 Not found:
not_found - 409 Crawl is not active:
crawl_not_active - 422 Validation failed:
validation_error - 503 Service busy:
capacity_busy