Skip to content

Receive and verify PageFox webhooks

Verify the signature, handle every event in the catalog, and plan for the real retry behaviour.

Signed with HMAC-SHA256 · one endpoint per organization · owner-managed

one delivery · lead.hot
POST
your HTTPS endpoint
X-PageFox-Event
lead.hot
X-PageFox-Delivery
evt_lead_hot_example
X-PageFox-Signature
t=<unix seconds>,v1=<hex hmac>
body
{ id, type, created_at, org_id, data }
[ 01 / 07 ] · Delivery

What a delivery looks like.

PageFox POSTs a JSON body to the single HTTPS endpoint your organization has configured, for each event type that endpoint subscribes to. Every delivery carries three headers.

Every body has the same five-key envelope: id, type, created_at, org_id, and a per-event data object.

HeaderContains
X-PageFox-EventThe event type, identical to the body's type.
X-PageFox-DeliveryA UUID unique to this delivery, identical to the body's id. Use it to make your handler idempotent.
X-PageFox-Signaturet=<unix seconds>,v1=<hex hmac>
[ 02 / 07 ] · Verify

Verify the signature.

Compute HMAC-SHA256 over the string `${t}.${rawBody}` using your signing secret, hex-encode it, and compare it to the v1 value in constant time. Two details decide whether this works:

  • Use the raw request bytes. Re-serializing the parsed JSON changes key order and whitespace, which changes the signature. Capture the body as a string or buffer before your framework parses it.
  • Bound the timestamp yourself. PageFox stamps t but does not enforce a window on your behalf. The reference implementation rejects anything more than 300 seconds away from your clock in either direction; that value is the recommended default, not a PageFox-side rule.

A verification failure is a rejection, never a retry-worthy error on your side: respond 400 and drop the delivery.

Node.js
import { createHmac, timingSafeEqual } from 'node:crypto'

// rawBody MUST be the exact bytes PageFox sent. Do not re-serialize the
// parsed JSON: key order and whitespace would change the signature.
export function verifyPageFoxSignature(secret, signatureHeader, rawBody, toleranceSeconds = 300) {
  const parts = new Map(
    signatureHeader.split(',').map((pair) => {
      const eq = pair.indexOf('=')
      return [pair.slice(0, eq).trim(), pair.slice(eq + 1).trim()]
    }),
  )
  const timestamp = parts.get('t')
  const signature = parts.get('v1')
  if (!timestamp || !signature) return false

  const ts = Number(timestamp)
  if (!Number.isFinite(ts)) return false
  if (Math.abs(Math.floor(Date.now() / 1000) - ts) > toleranceSeconds) return false

  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(signature, 'hex')
  return a.length === b.length && timingSafeEqual(a, b)
}
[ 03 / 07 ] · Privacy

What the payloads deliberately omit.

Lead and company payloads pass through the same privacy rules the dashboard and the REST API enforce, so the examples below are what you will actually receive, not a maximal shape.
  • Person-level enrichment never ships unless your organization holds the contact-reveal entitlement, which is not available yet. A lead.* payload keeps company-level enrichment such as industry and employee count, and simply has no person block.
  • A metering-locked company is redacted, not partially named. On lead.* you receive company_locked: true with company and enriched_data nulled. A company.* event for a locked company is not sent at all: its entire content would be the identity.
  • Demo websites emit nothing. Demo data never reaches your endpoint.
lead.hot: metering-locked company
{
  "id": "evt_lead_hot_locked_example",
  "type": "lead.hot",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "id": "lead_example_1",
    "name": "Jane Visitor",
    "email": "jane@northwind.example",
    "phone": null,
    "company": null,
    "company_locked": true,
    "score": 88,
    "classification": "hot",
    "priority": "high",
    "status": "new",
    "source_url": "https://example.com/pricing",
    "referrer": "https://www.google.com/",
    "suggested_action": "Reply while they are still on the pricing page.",
    "enriched_data": null,
    "created_at": "2026-08-20T14:00:00.000Z",
    "updated_at": "2026-08-21T11:15:00.000Z"
  }
}
[ 04 / 07 ] · Events

Event catalog.

Nine event types. An endpoint subscribes to any subset, or to all of them. Each example below is generated from the code that emits it and verified against it on every build.

lead.hot

Lead: High priority. A lead reached (or was upgraded to) High priority.

lead.hot
{
  "id": "evt_lead_hot_example",
  "type": "lead.hot",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "id": "lead_example_1",
    "name": "Jane Visitor",
    "email": "jane@northwind.example",
    "phone": null,
    "company": "Northwind Example",
    "company_locked": false,
    "score": 88,
    "classification": "hot",
    "priority": "high",
    "status": "new",
    "source_url": "https://example.com/pricing",
    "referrer": "https://www.google.com/",
    "suggested_action": "Reply while they are still on the pricing page.",
    "enriched_data": {
      "industry": "Software",
      "employee_count": 84
    },
    "created_at": "2026-08-20T14:00:00.000Z",
    "updated_at": "2026-08-21T11:15:00.000Z"
  }
}

lead.warm

Lead: Medium priority. A lead reached (or was upgraded to) Medium priority.

lead.warm
{
  "id": "evt_lead_warm_example",
  "type": "lead.warm",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "id": "lead_example_1",
    "name": "Jane Visitor",
    "email": "jane@northwind.example",
    "phone": null,
    "company": "Northwind Example",
    "company_locked": false,
    "score": 88,
    "classification": "warm",
    "priority": "medium",
    "status": "new",
    "source_url": "https://example.com/pricing",
    "referrer": "https://www.google.com/",
    "suggested_action": "Reply while they are still on the pricing page.",
    "enriched_data": {
      "industry": "Software",
      "employee_count": 84
    },
    "created_at": "2026-08-20T14:00:00.000Z",
    "updated_at": "2026-08-21T11:15:00.000Z"
  }
}

visitor.hot

Visitor worth calling. Somebody worth calling is on the site, and they have not left an address.

visitor.hot
{
  "id": "evt_visitor_hot_example",
  "type": "visitor.hot",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "previous_plan": "growth",
    "new_plan": "business"
  }
}

lead.captured

Lead captured. A visitor became an identified lead for the first time.

lead.captured
{
  "id": "evt_lead_captured_example",
  "type": "lead.captured",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "id": "lead_example_1",
    "name": "Jane Visitor",
    "email": "jane@northwind.example",
    "phone": null,
    "company": "Northwind Example",
    "company_locked": false,
    "score": 88,
    "classification": "hot",
    "priority": "high",
    "status": "new",
    "source_url": "https://example.com/pricing",
    "referrer": "https://www.google.com/",
    "suggested_action": "Reply while they are still on the pricing page.",
    "enriched_data": {
      "industry": "Software",
      "employee_count": 84
    },
    "created_at": "2026-08-20T14:00:00.000Z",
    "updated_at": "2026-08-21T11:15:00.000Z"
  }
}

agent.visited

AI agent visited. A verified AI agent or an AI-referred human visit was recorded.

agent.visited
{
  "id": "evt_agent_visited_example",
  "type": "agent.visited",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "website_id": "00000000-0000-4000-8000-000000000000",
    "visitor_id": "visitor_example_1",
    "conversation_id": null,
    "actor_type": "verified_agent",
    "ai_class": "assistant_fetch",
    "confidence": "high",
    "decision": "allow",
    "evidence_source": "signature"
  }
}

quota.warning

Tracking quota warning. The daily tracking-event quota crossed 80% or 100%.

quota.warning
{
  "id": "evt_quota_warning_example",
  "type": "quota.warning",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "website_id": "00000000-0000-4000-8000-000000000000",
    "threshold": 80,
    "used": 8000,
    "quota": 10000,
    "day": "2026-08-21",
    "website_breakdown": [
      {
        "websiteId": "00000000-0000-4000-8000-000000000000",
        "eventsCount": 8000
      }
    ]
  }
}

crawl.failed

Crawl failed. A website crawl job failed.

crawl.failed
{
  "id": "evt_crawl_failed_example",
  "type": "crawl.failed",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "website_id": "00000000-0000-4000-8000-000000000000",
    "failure_reason": "Crawl timed out after 300s"
  }
}

subscription.changed

Subscription changed. The org’s billing plan changed.

subscription.changed
{
  "id": "evt_subscription_changed_example",
  "type": "subscription.changed",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "previous_plan": "growth",
    "new_plan": "business"
  }
}

company.identified

Company identified. A company was identified on your site for the first time.

company.identified
{
  "id": "evt_company_identified_example",
  "type": "company.identified",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "id": "company_example_1",
    "name": "Northwind Example",
    "domain": "northwind.example",
    "industry": "Software",
    "employee_count": 84,
    "location": "Toronto, CA",
    "heat_band": "hot",
    "pages_viewed": 8,
    "page_url": "https://example.com/pricing",
    "dashboard_url": "https://pagefox.co/dashboard/leads?view=companies&companyId=company_example_1"
  }
}

company.returned

Company returned. A previously identified company came back for a new qualifying visit.

company.returned
{
  "id": "evt_company_returned_example",
  "type": "company.returned",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "id": "company_example_1",
    "name": "Northwind Example",
    "domain": "northwind.example",
    "industry": "Software",
    "employee_count": 84,
    "location": "Toronto, CA",
    "heat_band": "hot",
    "pages_viewed": 8,
    "page_url": "https://example.com/pricing",
    "dashboard_url": "https://pagefox.co/dashboard/leads?view=companies&companyId=company_example_1"
  }
}

lead.draft_ready

Lead: email draft ready. A checked first email or follow-up for a lead is ready to send, with its subject and body.

lead.draft_ready
{
  "id": "evt_lead_draft_ready_example",
  "type": "lead.draft_ready",
  "created_at": "2026-08-21T11:15:00.000Z",
  "org_id": "9f1d2c3b-4a5e-4f60-8712-0c9a1b2d3e4f",
  "data": {
    "lead_id": "lead_example_1",
    "email": "jane@northwind.example",
    "company": "Northwind Example",
    "company_domain": "northwind.example",
    "draft_id": "draft_example_1",
    "subject": "The Pune plant",
    "body": "Hi Jane,\n\nCongrats on the second plant in Pune. Is dispatch on your plate right now?\n\nRavi",
    "trigger": "top_picks"
  }
}
[ 05 / 07 ] · Retries

Retries, timeouts, and endpoint health.

3 attempts maximum per event, including the first. PageFox stops at the first 2xx.

Backoff of 500ms, then 1000ms between attempts (exponential from a 500ms base).

10-second timeout per attempt. A slow handler is a failed attempt, so acknowledge quickly and do your work asynchronously.

Redirects are not followed. A 3xx counts as a failure. Configure the final URL directly.

After 2 consecutive failed deliveries the endpoint is flagged in the dashboard so you can fix and reconnect it. Each attempt is recorded and the last ten are listed on the webhooks settings page.

There is no dead-letter queue and no manual redelivery. Once 3 attempts fail, that event is not retried again. Reconcile against the REST API if your handler was down.

[ 06 / 07 ] · Secret

Managing the secret and the endpoint.

The signing secret is generated by PageFox, prefixed whsec_, and shown exactly once: on first connect and again on each rotation. It is encrypted at rest and never returned to the dashboard afterwards, so store it in your secret manager immediately. If you lose it, rotate to get a new one; the old secret stops signing at that moment, so deploy the new one first if you can.

Who can configure this: the organization owner only. Members can view the endpoint's status and recent deliveries but cannot connect, rotate, disconnect, or send a test event. Webhooks are included on Growth and above; on Free the endpoint can be seen but not connected or changed.

The endpoint URL must be HTTPS and publicly resolvable; private and loopback addresses are rejected. One endpoint per organization. Configure it in Integrations → Outbound Webhooks, where a test delivery is also available.

[ 07 / 07 ] · Not yet

What is not available yet.

PageFox does not currently publish inbound webhooks, per-event-type endpoint URLs, a delivery replay API, or a webhook SDK. Contact-reveal person fields are not shipping on any channel yet. Nothing on this page describes a planned capability: everything here is live behaviour.

Your next lead may already be on your site.