# Needle Webhooks

[Needle](https://needle.app) is an agent-native outbound and GTM engine (not Needle Engine, the 3D tool). It sends one signed `POST` per event, such as a new lead, a LinkedIn or Instagram reply, a finished sequence or a disconnected account. Signatures follow **Standard Webhooks**.

## When to Use This Skill

* How do I receive Needle webhooks?
* How do I verify a Needle webhook signature?
* Where do I get my Needle `whsec_` webhook secret?
* How do I handle `lead_created`, `linkedin_message_received` or `enrollment_completed`?
* Why is my Needle `webhook-signature` verification failing?
* Why are Hookdeck retries or replays of Needle events rejected as too old?

## Signature at a Glance

| Property | Value |
| --- | --- |
| Headers | `webhook-id`, `webhook-timestamp`, `webhook-signature` |
| Signature header format | One or more space-separated `v1,<base64>` entries |
| Algorithm | HMAC-SHA256, base64 output |
| Signed content | `{webhook-id}.{webhook-timestamp}.{raw request body}` |
| Secret | `whsec_...`, shown once when the webhook is added (`NEEDLE_WEBHOOK_SECRET`) |
| Key | Base64-decode the part after `whsec_` |
| Timestamp | Unix seconds. The `standardwebhooks` library rejects anything more than 5 minutes off |
| Dedupe key | `webhook-id` (unchanged on a resend) |

## Verification (core)

Use the Standard Webhooks library (`standardwebhooks` on npm and PyPI). It checks the timestamp, recomputes the HMAC and compares in constant time against every `v1,` entry. **Pass the raw body**, not re-serialised JSON.

```javascript
const { Webhook } = require('standardwebhooks');

// rawBody: express.raw({ type: '*/*' }) buffer as a string, or await request.text() in Next.js
function verifyNeedleWebhook(rawBody, headers) {
  const wh = new Webhook(process.env.NEEDLE_WEBHOOK_SECRET); // full "whsec_..." value
  return wh.verify(rawBody, {
    'webhook-id': headers['webhook-id'],
    'webhook-timestamp': headers['webhook-timestamp'],
    'webhook-signature': headers['webhook-signature'],
  }); // returns the parsed JSON body; throws -> respond 400
}

```

Python: `from standardwebhooks.webhooks import Webhook`, then `Webhook(secret).verify(await request.body(), headers)`.

> **Hookdeck retries and replays vs the 5-minute window.** The library always enforces a fixed 5-minute timestamp window. Needle defines `webhook-timestamp` as "when this request was sent", so its own retries (right away, +10 min, +1 h) carry a fresh one. Hookdeck retries and replays resend the **original** `webhook-timestamp`, so the library rejects any of those that arrive more than 5 minutes after Needle sent the request. If your receiver sits behind Hookdeck, or you replay events, use the manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/needle-webhooks/references/verification.md), where the window can be dropped or widened.

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

## Event Payload

```json
{
  "version": "v1",
  "id": "00000000-0000-0000-0000-000000000001",
  "type": "linkedin_message_received",
  "timestamp": "2026-01-01T12:00:00.000Z",
  "data": {
    "id": "00000000-0000-0000-0000-000000000001",
    "type": "linkedin_message_received",
    "label": "Reply received",
    "occurredAt": "2026-01-01T12:00:00.000Z",
    "channel": "linkedin",
    "leadId": "00000000-0000-0000-0000-000000000002",
    "campaignId": "00000000-0000-0000-0000-000000000003",
    "data": { "text": "Hello!" }
  }
}

```

Dispatch on the top-level `type`. The inner `data.data` depends on the type. Needle adds event types over time, so ignore ones you don't know and still return `200`.

## Common Event Types

| Event | Triggered When |
| --- | --- |
| `lead_created` | A lead is created (app, API, MCP, import, or a direct message to a non-lead) |
| `lead_updated` | A lead's details or handles change (`data.data.changes`) |
| `linkedin_message_received` | A LinkedIn reply arrives |
| `enrollment_completed` | A lead reaches the end of a sequence |
| `campaign_status_changed` | A campaign is started, paused, etc. |
| `account_disconnected` | A connected account needs signing in again |

The docs list 67 types. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/needle-webhooks/references/overview.md) for the groups.

## Delivery Semantics

* Reply `2xx` within 10 seconds. Otherwise the event is tried 3 times: right away, after 10 minutes and 1 hour after the first try. After 3 days it is given up.
* A `429` makes Needle pause sending to your URL for a few seconds.
* Delivery is at least once and **unordered**: up to 5 requests can be open at once. Dedupe on `webhook-id` and use `timestamp` for ordering.
* Custom headers you add to the webhook are sent but are **not signed**. Don't treat them as proof of origin on their own.

## Environment Variables

```bash
NEEDLE_WEBHOOK_SECRET=whsec_xxxxx   # shown once when you add the webhook; handlers fail closed without it

```

## Local Development

Receive webhooks locally with the Hookdeck CLI. No install or account required:

```bash
npx hookdeck-cli listen 3000 needle --path /webhooks/needle

```

Use the printed URL (it must be `https://`) as the webhook URL in Needle. FastAPI examples use port `8000`.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/needle-webhooks/references/overview.md) - Event groups, payload fields, delivery and retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/needle-webhooks/references/setup.md) - Add a webhook, pick event types, custom headers
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/needle-webhooks/references/verification.md) - Library and manual verification, dropping the window, debugging