Skip to content
API Reference

PerfBee API and Webhooks: Keys, Crawls and Limits

7 min read · Updated Nov 4, 2026

Make an API key, read your crawl results, start a crawl and wait for it, and get a webhook when a crawl's data is ready. Every plan; covers the limits, the error codes and what the API cannot do.

The PerfBee API lets a script read your crawl results and start crawls. Webhooks tell your tools the moment a crawl's data is ready. The REST API, webhooks and the MCP server are on every plan. Reading costs no credits. A crawl you start through the API spends credits at the same rates as a crawl you start in the dashboard.

Live Google Sheets links, and exports through the API, are for Starter and up.

PerfBee crawls from its own cloud. It cannot reach a private network or a VPN, so a site must be open to the internet to be crawled. For a staging site, see Crawling a Private Staging Site Through a Tunnel.

Get a key

Open Settings, then API keys, and make a key. Give it a name, choose its Access (Read only or Read and start crawls) and when it Expires (Never, 30 days, 90 days or 1 year). Tick Also manage webhooks if the key will manage webhook endpoints. These choices set the scopes below. The key is shown once. Copy it into your secret store. A workspace can have 5 active keys.

A key carries scopes:

  • read: every list and detail the API offers, and live links, except webhook endpoints and their deliveries, which need the webhooks scope. This is the scope of most scripts.
  • crawl: start and cancel a crawl. It includes read.
  • webhooks: make, change, test and delete webhook endpoints, rotate their secrets and read their deliveries.

A key that can start crawls also carries a daily credit cap: 2,000 credits unless you choose another number or no cap. The cap counts the most each crawl could cost, at the moment it starts, per day (UTC). A key made before the REST API existed has no cap and can start crawls of at most 500 pages. Make a new key to go higher.

Viewers cannot make keys. A key acts with the role of the person who made it, checked on every call. If that person becomes a viewer, the key can only read, and it cannot make or revoke live links either. If they leave the workspace, the key stops working.

Send the key as Authorization: Bearer pbk_.... A key in the address, a cookie or a dashboard login is refused. A web page on another site cannot call the API from a browser: keep keys on a server or in CI, never in browser code.

Your first call

curl -s https://dashboard.perfbee.com/api/v1/me -H "Authorization: Bearer pbk_..."

GET /me shows the workspace, the plan, the credits left this month with the rate per page, and what the key may do. Every list answers {"data": [], "next_cursor": null, "has_more": false}. Pass next_cursor back as cursor for the next page. A page holds 50 items unless you ask for more with limit, and at most 200 (the issues list: 100).

Start a crawl and wait for it

  1. POST /crawls with {"site_id": 12, "mode": "fast", "max_pages": 500} and an Idempotency-Key header. The site must be in My Sites; GET /sites lists them. The answer is 202 with the crawl. The same key and body sent again return the same crawl and start nothing. A key is remembered for 24 hours.
  2. GET /crawls/{id}?wait=25, repeated, holds for up to 25 seconds and returns as soon as the crawl ends. Stop when status is completed, failed, cancelled or partial.
  3. GET /crawls/{id}/diff says what changed since the previous crawl of the site in the same mode, for example summary.new_broken.

A completed crawl's pages and fixes are added a little later. processing on the crawl says where they stand: inventory (the pages) and fixes read pending until they are in, then done. They can also read failed, skipped or not_applicable, so do not wait for done without a time limit. The Crawl data ready webhook is the exact signal. Only a completed crawl has a diff or an issues list. For any other status the answer is crawl_not_ready (409).

The most a crawl can cost, max_pages times the rate per page, is checked before it starts. If the workspace has fewer free credits than that, the answer is 402. Free credits are the credits left this month, less what the workspace's other running API crawls may still spend. You are charged when the crawl ends, for the pages it checked. A crawl can ask for up to 10,000 pages.

To protect your site and your credits, a key can start 10 crawls a day and a workspace 20. At most 2 crawls started through the API run at once in a workspace. Crawls of one site through the API must be 10 minutes apart. The crawler follows robots.txt and its usual pace, as in the dashboard.

Webhooks

In Settings, Webhooks, add an address and choose the events. A workspace can have 5 endpoints. An address must be https and public. The event Crawl data ready (scan_data_ready) arrives once for each crawl and site when the crawl's pages, checks and fixes are in place. Scan completed arrives when the crawl itself ends, which can be a little earlier. The same endpoints can be managed with the API and a key with the webhooks scope.

Every delivery to an endpoint with a signing secret is signed. The header PerfBee-Signature looks like t=1700000000,v1=.... v1 is the hex HMAC-SHA256 of t, a dot and the raw body, keyed with the endpoint's secret (a secret PerfBee makes starts with whsec_ and is shown once, when you make or rotate it). To check a delivery:

  • Use the raw bytes of the body, not JSON written again.
  • Refuse it when t is more than 300 seconds from your clock.
  • Compare the HMAC with each v1 in constant time. One match is enough.
  • Answer with a 2xx status. Redirects are not followed, so a 3xx counts as a failure.

When you rotate a secret, the old one keeps signing for 24 hours and the header carries two v1 values, so you can switch without a gap. Some failures may pass: no answer within 10 seconds, a connection error, 408, 425, 429 or a 5xx status. PerfBee tries those again, up to 5 attempts in all, waiting 1, 5, 30 and 120 minutes between them. Other answers, such as 404 or a redirect, end the delivery at once. The event id (PerfBee-Event-Id, and id in the body) is the same on every attempt: use it to ignore an event you already handled. Delivery records are kept for 14 days. A failed delivery keeps its body for 3 days, and you can send it again from the deliveries list. After 5 failed attempts in a row you get a notice in PerfBee.

An endpoint that was set up before signed webhooks existed is moved into the list of endpoints and shows Moved from before. If it had a secret, its deliveries are signed with that secret, which may not start with whsec_. If it never had one, it shows Unsigned and sends no PerfBee-Signature until you choose Add secret.

A moved endpoint also sends the old headers X-PerfBee-Event, X-PerfBee-Delivery and X-PerfBee-Attempt for 90 days after it was moved. They carry the same values as PerfBee-Event, PerfBee-Event-Id and PerfBee-Attempt. With a secret, it also sends the plain X-PerfBee-Secret header for those 90 days. Adding or rotating a secret stops all of these old headers.

Limits

  • Calls: 60 a minute for each key. Calls that read a lot of data (lists of links, issues, pages, the comparison, exports) also count against 20 a minute. Starting a crawl counts against 10 a minute. A workspace can make 240 calls a minute across its keys. Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A refused call carries Retry-After.
  • Time: a query that runs longer than 15 seconds is answered with 503. Narrow the filters and try again.
  • Exports: a CSV download holds up to 250,000 rows and an Excel file up to 150,000. A bigger report is refused with 413, never cut. Download it from the dashboard.
  • Live links: at most 50,000 rows and 2,000,000 cells each, 20 for a workspace, 12 reads an hour each, and a link stops working after 365 days. A site with staging access cannot have one.

When a call fails

Errors are application/problem+json with a stable code, a sentence in detail and a request_id. Quote the request id when you write to support. Switch on code, not on the sentence. The ones you will meet most:

  • invalid_key (401): the key is missing, wrong, revoked or expired.
  • scope_required (403): the key lacks the scope the call needs.
  • plan_required (403): the plan does not include what the call needs. Free has no exports or live links, the ai-suggestions export needs AI suggestions (Business and up), and the elements export needs Content Monitor (Enterprise).
  • insufficient_credits (402): not enough credits left this month. A credit shortage is never a 429.
  • not_found (404): the object does not exist, or belongs to another workspace. The two are never told apart.
  • crawl_in_progress and crawl_too_soon: a crawl of the site is running, or the last crawl started through the API began less than 10 minutes ago.
  • rate_limited, daily_credit_cap_reached, crawl_limit_reached (429): wait for Retry-After.
  • invalid_cursor (400): start the list again without a cursor.

What the API cannot do

  • It does not add or remove sites, verify ownership or change schedules. Do that in the dashboard.
  • It does not run performance tests, Critical CSS, competitor analysis or Real User Monitoring, and it does not edit Content Monitor.
  • It cannot mark a fix as resolved or run Check if fixed.
  • It does not manage billing, team members or API keys.
  • It never fetches your site by itself. A crawl is the same crawl the dashboard starts.

Within version 1 the API only grows. New fields, new event names and new error codes can appear, so ignore what you do not know. A change that breaks a client would be a new version, /api/v2, with at least six months of overlap.