Skip to content
Developers

Webhooks

PerfBee sends a signed POST to your address when something happens. You verify it, answer 2xx, and read the details through the API.

Set up an endpoint

  1. In the dashboard open Settings > Webhooks, or call POST /webhook-endpoints with a key that has the webhooks scope.
  2. Give an https address on a public host, and pick the events from the list below. A private address is refused. The address is checked again at every delivery, and redirects are not followed.
  3. Copy the signing secret. It starts with whsec_ and is shown once, in the answer that makes the endpoint. PerfBee keeps it encrypted.
  4. Send a test event from the dashboard or with POST /webhook-endpoints/{endpoint_id}/test. It is signed like any other.

A workspace can have 5 endpoints. Each can take any events.

What is sent

Each delivery is one POST with a JSON body and these headers.

HeaderMeaning
Content-TypeAlways application/json.
PerfBee-EventThe event name.
PerfBee-Event-IdThe id of the event. It is the same on every attempt and on every redelivery: use it to ignore a repeat.
PerfBee-AttemptThe number of this attempt, from 1.
PerfBee-SignatureThe signature of this attempt: see below.

The body has these members.

MemberMeaning
idThe event id, the same as PerfBee-Event-Id.
api_versionThe version of the payload format.
eventThe event name.
event_descriptionThe event in words.
timestampWhen the event was made, ISO 8601 in UTC.
dataWhat happened. Members depend on the event, and new members can appear.
An example delivery body
{
  "id": "evt_5f0c2b7e9a1d4c3e8b6a7d9f01234567",
  "api_version": "v1",
  "event": "scan_data_ready",
  "event_description": "Crawl data ready",
  "timestamp": "2026-11-01T09:30:12.512Z",
  "data": {
    "crawl_id": "0f7c9a52-3d4b-4a39-8c0e-5e2a9d1b6f10",
    "mode": "full",
    "site_id": 7
  }
}

A client should ignore members it does not know: within v1 new members are added, never removed. The data of an event that names a crawl includes crawl_id, site_id and report_url, and an api_url to read the crawl.

Check the signature

Every delivery carries PerfBee-Signature: t=<unix seconds>,v1=<hex>. Check it before you trust the body.

  1. Read the raw bytes of the request body. Do not parse and write the JSON again: the signature covers the exact bytes.
  2. Take t and every v1 from the header.
  3. Refuse the delivery if t is more than 300 seconds away from your clock. Every attempt is signed again with its own time.
  4. Compute HMAC-SHA256 over <t>.<raw body>, that is the time, a dot and the raw body, with the signing secret as the key. Use the secret exactly as shown, whsec_ included. Write the result as hex.
  5. Compare it to each v1 with a constant-time comparison. One match is enough. There are two while a secret is rotated.

Working code is in the recipe Verify a webhook signature.

Events

EventGroupWhen it is sent
scan_startedSite CrawlerA crawl began, started by hand or by a schedule.
scheduled_scan_triggeredSite CrawlerA schedule put its scan in the queue.
scan_data_readySite CrawlerA crawl's pages, checks and fixes are in place, once per site after each crawl. Read them through the API.
broken_links_foundSite CrawlerA crawl finished with at least one broken link.
scan_completedSite CrawlerEvery finished crawl, with or without broken links.
scan_failedSite CrawlerA crawl aborted before finishing.
crawler_signature_expiringSite CrawlerThe crawler signature a Shopify store gave us runs out in two weeks; after that the crawl is refused.
site_verification_lostSite CrawlerThe weekly check could not find the DNS record, file or meta tag that proves a site is yours.
fast_lane_offSite CrawlerA scan of a site in the fast lane was refused, so its scans go at the standard pace again.
ai_access_lostSite CrawlerAn AI search crawler (ChatGPT, Claude, Perplexity, Google) could reach the site last scan and cannot now.
content_driftContent MonitorA monitored element disappeared from pages or broke a rule.
stock_changedMonitorsProducts of a site in Stock watch ran out of stock or came back.
performance_alertMonitorsA scheduled performance check crossed one of its thresholds.
css_changedMonitorsA scheduled Critical CSS check saw the stylesheet change beyond its threshold.
credit_lowAccountLess than 20% of the monthly credits remain.
credit_exhaustedAccountNo credits left; scheduled scans are skipped until the cycle resets.
fix_check_doneSite CrawlerA Check if fixed you started in Fixes has its result.
fixes_resolvedSite CrawlerA scan found that fixes on the Fixes list are done.
scheduled_scan_skippedAccountA scheduled scan did not start because the credits left do not cover it. Once per schedule a month.

Start from scan_data_ready when you want to read a crawl. scan_completed can arrive while the crawl's pages and fixes are still being processed. scan_data_ready is sent once per crawl and site, after that is done. The list of events is also served by GET /webhook-events.

Your answer and retries

  • Answer with any 2xx within 10 seconds. Do the work after you have answered.
  • A timeout, a failed connection, a status from 500 up and the statuses 408, 425 and 429 are tried again. Any other answer ends the delivery as failed.
  • A delivery gets 5 attempts in all. The waits between them are 1, 5, 30 and 120 minutes.
  • After 5 failed attempts in a row the workspace gets an in-app notice that the endpoint is failing.

A delivery can arrive twice, and deliveries can arrive out of order. Use id to ignore a repeat. When the order matters, read the current state through the API instead of trusting the order of arrival.

Deliveries and redelivery

GET /webhook-endpoints/{endpoint_id}/deliveries lists what was sent, with the status of the last attempt. Delivery records are kept 14 days. A failed delivery also keeps its body for 3 days, and during that time POST /webhook-endpoints/{endpoint_id}/deliveries/{delivery_id}/redeliver sends it again with the same id. A delivered event keeps no body.

Rotate the secret

POST /webhook-endpoints/{endpoint_id}/rotate-secret makes a new secret and shows it once. The old one keeps signing for 24 hours, so every delivery in that time has two v1 values. Switch your receiver to the new secret in that time.

Endpoints from before signing

The single endpoint that Settings > Webhooks had before is moved into the list the first time it is needed. It keeps its secret, and the signature is made with it. For 90 days after the move it also sends that secret in the plain header X-PerfBee-Secret, with X-PerfBee-Event, X-PerfBee-Delivery and X-PerfBee-Attempt. Check the signature instead: the plain headers stop after that time. An endpoint that had no secret is unsigned until you rotate one.