Agent skill
Needle Webhooks Skill
Receive and verify Needle webhooks. Use when setting up Needle (needle.app) webhook endpoints, debugging Standard Webhooks signature verification (webhook-id, webhook-timestamp, webhook-signature, whsec_ secret), or handling outbound events like lead_created, linkedin_message_received, enrollment_completed, campaign_status_changed and account_disconnected.
Install this skill
npx skills add hookdeck/webhook-skills --skill needle-webhooks
Needle 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_ormessage_ received enrollment_completed? - Why is my Needle
webhook-signatureverification 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.
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-timestampas "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 originalwebhook-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, where the window can be dropped or widened.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Payload
{
"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_ | 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 for the groups.
Delivery Semantics
- Reply
2xxwithin 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
429makes 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-idand usetimestampfor 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
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:
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 - Event groups, payload fields, delivery and retries
- references/setup.md - Add a webhook, pick event types, custom headers
- references/verification.md - Library and manual verification, dropping the window, debugging