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_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

PropertyValue
Headerswebhook-id, webhook-timestamp, webhook-signature
Signature header formatOne or more space-separated v1,<base64> entries
AlgorithmHMAC-SHA256, base64 output
Signed content{webhook-id}.{webhook-timestamp}.{raw request body}
Secretwhsec_..., shown once when the webhook is added (NEEDLE_WEBHOOK_SECRET)
KeyBase64-decode the part after whsec_
TimestampUnix seconds. The standardwebhooks library rejects anything more than 5 minutes off
Dedupe keywebhook-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-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, 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

EventTriggered When
lead_createdA lead is created (app, API, MCP, import, or a direct message to a non-lead)
lead_updatedA lead's details or handles change (data.data.changes)
linkedin_message_receivedA LinkedIn reply arrives
enrollment_completedA lead reaches the end of a sequence
campaign_status_changedA campaign is started, paused, etc.
account_disconnectedA connected account needs signing in again

The docs list 67 types. See 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

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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 6, 2026

View on GitHub →