> ## Documentation Index
> Fetch the complete documentation index at: https://findprospectsnow.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook payload reference

> The JSON payload Find Prospects Now sends to custom webhooks after a scan, its headers, and how to verify the HMAC-SHA256 signature.

Source: https://findprospectsnow.com/docs/notifications/webhook-payload  
Last updated: 2026-10-17

Custom (non-Slack, non-Discord) webhooks receive one `POST` request with a JSON body per scan that finds new matches.

## Example

```json
{
  "schema_version": 1,
  "event_id": "6f1c2a8e-1b0d-4b7a-9d3e-2f4c5a6b7c8d",
  "event": "scan_completed",
  "timestamp": "2026-10-17T14:05:12.000Z",
  "scan_id": "a3b4c5d6-…",
  "summary": {
    "total_matches": 2,
    "total_processed": 180,
    "groups_scanned": 9,
    "scan_type": "scheduled",
    "truncated_leads": 0
  },
  "leads": [
    {
      "post_id": "1234567890",
      "group_name": "Austin Homeowners",
      "author": "Jane Doe",
      "text": "Can anyone recommend a plumber? Our water heater is leaking.",
      "url": "https://www.facebook.com/groups/…/posts/…",
      "created_at": "2026-10-17T13:52:00.000Z",
      "matched_keywords": ["recommend a plumber", "leaking"],
      "match_type": "keyword",
      "ai_classified": true,
      "ai_match": true,
      "ai_label": "Plumbing request"
    }
  ]
}
```

## Fields

| Field | Description |
| --- | --- |
| `event` | `scan_completed` for real scans. **Test** sends the same shape with `scan_type: "test"` and a sample lead. |
| `event_id` | Unique ID of this event. Use it to ignore duplicates. |
| `timestamp` | When the scan finished (ISO 8601). |
| `summary.total_matches` | Number of matching posts in the scan. |
| `summary.total_processed` | Posts checked. |
| `summary.groups_scanned` | Groups scanned. |
| `summary.scan_type` | How the scan started, for example `manual`, `scheduled` or `test`. |
| `summary.truncated_leads` | Leads not included because a payload holds at most 100 leads. |
| `leads[].post_id` | Facebook post ID. |
| `leads[].group_name` | Group name. |
| `leads[].author` | Author's display name. |
| `leads[].text` | Post text. |
| `leads[].url` | Link to the post on Facebook. |
| `leads[].created_at` | When the post was published, or `null` if unknown. |
| `leads[].matched_keywords` | Keywords that matched (may be empty). |
| `leads[].match_type` | `keyword` or `ai`. |
| `leads[].ai_classified` | Whether AI checked the post. |
| `leads[].ai_match` | Whether an AI criterion matched. |
| `leads[].ai_label` | Label of the matching AI criterion, or `null`. |

## Headers

| Header | Value |
| --- | --- |
| `Content-Type` | `application/json` |
| `User-Agent` | `FindProspectsNow/1.0` |
| `X-Webhook-Event` | The event name, e.g. `scan_completed` |
| `X-Webhook-Id` | The event ID |
| `X-FPN-Delivery-Id` | Unique ID of this delivery attempt series |
| `X-Webhook-Signature` | `sha256=<hex>` — only when a signing secret is set |

## Verify the signature

When you set a signing secret, compute the HMAC-SHA256 of the **raw request body** with your secret and compare it with the header.

```js title="Node.js"
import { createHmac, timingSafeEqual } from 'node:crypto';

function isValid(rawBody, header, secret) {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
  return header && expected.length === header.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
```

## Responding

Return any `2xx` status within a few seconds. Timeouts, `408`, `425`, `429` and `5xx` responses are retried a few times with increasing delays; other `4xx` responses are not retried. Process leads asynchronously if your work is slow, and deduplicate with `event_id`.
