# People Data Labs Webhooks

People Data Labs (PDL) sends a webhook when a data release updates person records that match the
filters registered for your account (for example `job_change`, `work_email` or `location`). The
body is a JSON array of PDL IDs, sent in batches of up to 1,000, roughly once a month. Webhooks
are available to Enterprise API and Data License customers only.

PDL webhooks are **not signed**. There is no signature header and no timestamp. You authenticate
a request with a secret header that you ask PDL to include when they register the webhook.

> The People Data Labs team registers webhooks for Enterprise customers. Ask them to include a secret header.

## When to Use This Skill

* How do I receive People Data Labs webhooks?
* How do I authenticate PDL webhook requests? (Compare the shared secret header)
* Does PDL sign webhook payloads? (No)
* How do I handle the batches of updated person IDs PDL sends after a data release?
* Why does my PDL webhook endpoint return 401?

## Verification (core)

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyPdlRequest(headers, {
  name = (process.env.PEOPLE_DATA_LABS_WEBHOOK_HEADER || 'Authorization').toLowerCase(),
  secret = process.env.PEOPLE_DATA_LABS_WEBHOOK_SECRET,
} = {}) {
  if (!secret) return false;           // never accept when unconfigured
  const received = headers[name.toLowerCase()]; // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret); // compare the WHOLE value
}

```

* The header name is whatever PDL was told to send. The docs' example is
  `"Authorization": "Bearer pk_1312323123131231"`. Set `PEOPLE_DATA_LABS_WEBHOOK_HEADER` to the
  name (default `Authorization`) and `PEOPLE_DATA_LABS_WEBHOOK_SECRET` to the **exact, whole
  value**, including any `Bearer ` prefix you typed. Do not strip or parse the prefix.
* Look the header up case-insensitively. A missing header or a mismatch returns `401`.
* Read the body raw (`express.raw({ type: '*/*' })`) so any content type works, check the header
  first, and parse the JSON only after that.
* A static shared credential authenticates the sender, not the body, and gives no replay
  protection (no signature, no timestamp). Use HTTPS only, and rotate the credential with the PDL
  team if it leaks.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/people-data-labs-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/people-data-labs-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/people-data-labs-webhooks/examples/fastapi/).

## Payload and Event Types

PDL has no event-type field. Every request is the same kind of event: a JSON array of PDL IDs for
records updated in a release.

```json
["9adnit66-hdZ5lkBbq-AOQ_0000", "YraxDoP16jzyjfxtn-1XJQ_0000", "6Wu6sKeyNfaLF3CVBgoysg_0000"]

```

| Event | Sent When |
| --- | --- |
| person record update batch | A data release updates records matching your registered filters (up to 1,000 IDs per request, about monthly) |

* Filters are chosen at registration and are not in the payload: `education`, `job_change`,
  `location`, `mobile_phone_number`, `personal_emails`, `phone_number` (watches the
  `mobile_phone` and `phone_numbers` fields), `social_profile`, `work_email`. With several
  filters on one webhook, an ID can appear more than once, so dedupe. Consider one webhook per
  filter.
* PDL does **not** retry. If you don't return `200`, ask your PDL team to resend the whole batch.
  Return `200` only once the IDs are stored, and make processing idempotent.

## Environment Variables

```bash
PEOPLE_DATA_LABS_WEBHOOK_HEADER=Authorization        # header name PDL sends (default Authorization)
PEOPLE_DATA_LABS_WEBHOOK_SECRET=Bearer your-shared-key # exact, whole header value

```

## Local Development

PDL can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 people-data-labs --path /webhooks/people-data-labs

```

Give the printed URL to your PDL team as the target URL.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/people-data-labs-webhooks/references/overview.md) - What PDL webhooks are, filters and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/people-data-labs-webhooks/references/setup.md) - Getting a webhook registered with a secret header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/people-data-labs-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/people-data-labs-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests