Gareth Wilson Gareth Wilson

Guide to Needle Webhooks: Features and Best Practices

Published


Needle webhooks tell your systems what is happening in your outbound campaigns: a lead is added, a prospect replies on LinkedIn or Instagram, a sequence finishes, a draft waits for approval, or a connected account needs signing in again. Needle is an agent-native outbound engine that runs LinkedIn, Instagram and email campaigns over MCP and REST, and webhooks are how that activity reaches your CRM, alerting and reporting without polling the Search events API.

This guide covers how Needle webhooks are added and signed, the event types and the body they share, Needle's short retry schedule, and the practices that keep a Needle receiver reliable. It is about Needle at needle.app, not Needle Engine, the 3D web engine, which has no webhooks.

What are Needle webhooks?

A Needle webhook is an HTTPS URL plus the event types it should receive. Any member of a workspace can add one, and a workspace can have up to five. When an event happens that the webhook's creator is allowed to see, Needle sends it as a JSON POST.

Every request is signed with the Standard Webhooks scheme: webhook-id, webhook-timestamp and webhook-signature headers, and a whsec_ secret per webhook that Needle shows only once.

Webhooks belong to the member who added them. A webhook receives only the events that member can see, and Needle deletes it when that member leaves the workspace. Pick the creator with that in mind.

Needle webhook features

FeatureDetails
ConfigurationSettings > Integrations, the Create a webhook API, or the needleCreateWebhook MCP tool
LimitUp to 5 webhooks per workspace
OwnershipOnly the creator can change a webhook. The creator or a workspace admin can delete it
VisibilityA webhook receives only events its creator can see, and is deleted when they leave
Endpoint URLMust start with https://
SubscriptionA list of event types, or "all" (also types Needle adds later)
Payload{ "version": "v1", "id", "type", "timestamp", "data" }
Signature headerswebhook-id, webhook-timestamp, webhook-signature
SchemeStandard Webhooks: HMAC-SHA256, base64, entries prefixed with v1,
Signed content{webhook-id}.{webhook-timestamp}.{raw body}
Secretwhsec_..., shown once when the webhook is added
Custom headersUp to 5, sent with every request and not signed
Success responseA 2xx within 10 seconds
RetriesRight away, 10 minutes later and 1 hour after the first try, then dropped. Given up after 3 days
OrderingAt least once and unordered, with up to 5 requests open at once
Source IPsNot published

Common events

Needle documents 67 event types and says it adds more over time. Two of them, linkedin_follow_sent and linkedin_unfollowed, are listed as "Not sent yet".

AreaEvent types
Leadslead_created, lead_updated, lead_deleted, lead_enrolled, lead_skipped, lead_suppressed
Sequencesmessage_generated, condition_evaluated, task_skipped, task_cancelled, task_failed, enrollment_waiting, enrollment_completed, enrollment_stopped
Drafts and approvalsdraft_created, draft_approved, draft_discarded, draft_failed, and the deprecated task_held and task_approved
Campaignscampaign_status_changed, campaign_updated
Connected accountsaccount_connected, account_reconnected, account_deleted, account_paused, account_resumed, account_disconnected, account_suspended, account_limits_updated, account_access_granted, account_access_revoked, account_sharing_changed
Workspacemember_invited, member_invitation_revoked, member_joined, member_role_changed, member_removed, member_left, api_key_created, api_key_revoked, api_key_enabled, api_key_deleted, webhook_created, webhook_updated, webhook_deleted, suppression_added, suppression_removed
LinkedInlinkedin_profile_visited, linkedin_invite_sent, linkedin_invite_withdrawn, linkedin_invite_accepted, linkedin_message_sent, linkedin_message_received, linkedin_post_commented, linkedin_post_reacted
Instagraminstagram_message_sent, instagram_message_received, instagram_profile_visited, instagram_follow_sent, instagram_unfollowed, instagram_post_commented, instagram_post_reacted, instagram_message_reacted, instagram_post_created

Every body has the same wrapper. Here is Needle's example, trimmed:

{
  "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",
    "nodeId": null,
    "leadId": "00000000-0000-0000-0000-000000000002",
    "leadName": "Jane Doe",
    "leadEmail": "jane@example.com",
    "campaignId": "00000000-0000-0000-0000-000000000003",
    "enrollmentId": "00000000-0000-0000-0000-000000000004",
    "connectedAccountId": "00000000-0000-0000-0000-000000000005",
    "data": { "text": "Hello!" }
  }
}
  • type is the event type, repeated as data.type. Route on it.
  • timestamp is when the event happened. Use it, not arrival order, to order events.
  • data is the same event object that Needle's Search events API returns. nodeId is the campaign step and enrollmentId is the lead's run through a campaign.
  • leadEmail and leadIdentities help you match the person in your own system. They are empty when the event has no lead.
  • The inner data.data depends on the type. For example, lead_updated carries changes, lead_skipped carries skipReason, enrollment_stopped carries exitReason and account_disconnected carries errorKind.
  • version is v1. Needle says it can add types and fields within v1 but won't remove or rename fields.

Setting up Needle webhooks

  1. In Needle, open Settings > Integrations and add a webhook.
  2. Enter your HTTPS endpoint URL.
  3. Choose the event types. Start from a preset: All events, Recommended (LinkedIn and Instagram activity plus leads added, suppressed leads, finished sequences, AI messages and approvals) or Engagements (replies only), then tick or untick single types. Turn on "Also send event types Needle adds later" if you want new types automatically.
  4. Save, and copy the signing secret straight away. It starts with whsec_ and Needle shows it only once. Store it server-side as NEEDLE_WEBHOOK_SECRET.

Through the API or MCP, eventTypes is either a list or "all", and "all" is the default when you leave it out. A list never grows on its own.

If your endpoint also expects its own key, add it as a custom header, such as Authorization: Bearer <your key>. A webhook can have up to five, Needle stores their names in lower case, and you can't override Needle's own headers. Changing the webhook's URL removes the saved headers, so add them again in the same change.

With Hookdeck. Create a source with the Needle source type, paste the whole whsec_ secret into Webhook Signing Secret, and use the Hookdeck source URL as the webhook URL. A delivery that fails the check still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't pass it on to your destination.

Securing Needle webhooks

To verify a delivery, join webhook-id, webhook-timestamp and the raw request body with dots. Take the part of the secret after whsec_, base64-decode it, and use the bytes as the HMAC-SHA256 key. Encode the result as base64 and compare it in constant time with each v1, entry in the space-delimited webhook-signature header. A match on any entry is enough.

The timestamp check below is optional and off by default. If you turn it on, remember that a delivery retried or replayed later, for example from Hookdeck, keeps its original timestamp.

const crypto = require('crypto');
const express = require('express');

function verifyNeedleSignature(rawBody, headers, secret, toleranceSeconds = 0) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatureHeader = headers['webhook-signature'];
  if (!id || !timestamp || !signatureHeader || !secret || !secret.startsWith('whsec_')) return false;
  if (!/^\d+$/.test(timestamp)) return false;

  // Optional replay check, off by default.
  if (toleranceSeconds > 0 && Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
    return false;
  }

  const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
  const expected = crypto
    .createHmac('sha256', key)
    .update(`${id}.${timestamp}.`)
    .update(rawBody)
    .digest();

  return signatureHeader.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const received = Buffer.from(signature, 'base64');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

const app = express();

app.post('/webhooks/needle', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyNeedleSignature(req.body, req.headers, process.env.NEEDLE_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on the webhook-id header, then route on event.type.
  res.sendStatus(200);
});

The same check in Python:

import base64
import binascii
import hashlib
import hmac
import time


def verify_needle_signature(raw_body: bytes, headers, secret: str, tolerance_seconds: int = 0) -> bool:
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signature_header = headers.get("webhook-signature")
    if not (msg_id and timestamp and signature_header and secret and secret.startswith("whsec_")):
        return False
    if not timestamp.isdigit():
        return False
    # Optional replay check, off by default.
    if tolerance_seconds > 0 and abs(time.time() - int(timestamp)) > tolerance_seconds:
        return False

    try:
        key = base64.b64decode(secret[len("whsec_"):])
    except (binascii.Error, ValueError):
        return False
    signed = f"{msg_id}.{timestamp}.".encode("utf-8") + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())

    for entry in signature_header.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature.encode("utf-8"), expected):
            return True
    return False


# In FastAPI: raw_body = await request.body()
# verify_needle_signature(raw_body, request.headers, os.environ["NEEDLE_WEBHOOK_SECRET"])

Needle's own Node and Python samples use the standardwebhooks library, which does the same check. Needle points out that the library also refuses a request whose webhook-timestamp is more than five minutes from your server's clock. That window is fixed, so a delivery that Hookdeck retries or replays more than five minutes after Needle sent it fails the library's check even though its signature is valid. The Needle webhook skill includes a manual verifier where you can drop or widen the window.

Custom headers aren't part of the signature. Treat a custom Authorization header as an extra check, not a replacement for verifying webhook-signature.

Make Needle webhooks production-ready. Hookdeck Event Gateway verifies the Standard Webhooks signature on every Needle delivery, stores each lead, reply and account event, and retries delivery to your services for as long as you configure, well past Needle's one-hour retry window.

Needle webhook limitations and pain points

Three tries and the event is gone

The Problem: An outage of a couple of hours loses events for good.

Why It Happens: An event fails when your URL doesn't answer with a 2xx within 10 seconds. Needle tries a failed event three times: right away, 10 minutes later and an hour after the first try. After the third failure it is not sent again, and anything still undelivered after three days is given up.

Workarounds:

  • Return 2xx within 10 seconds and do the work afterwards.
  • After an outage, fetch the missed events with Needle's Search events API. Needle keeps them in the event log.
  • Answer 429 only when you truly need Needle to back off. It pauses sending for a few seconds and retries those events on the same schedule.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives, so Needle sees a success even while your service is down. Hookdeck then delivers to your service with retries on your own schedule, and you can replay any stored event.

Events arrive out of order, sometimes twice

The Problem: A reply is processed before the message it answers, or the same event is handled twice.

Why It Happens: Delivery is at least once. Up to five requests to your URL can be open at once, and a retried event can arrive after newer ones.

Workarounds:

  • Deduplicate on webhook-id, which Needle keeps the same when it resends a request.
  • Order by the body's timestamp, which is when the event happened.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries before they reach your handler, and its delivery rate limit controls how many requests reach your service at once.

Webhooks belong to a person

The Problem: Events stop when someone leaves the company, or a webhook never sees some LinkedIn activity.

Why It Happens: A webhook receives only what its creator can see, and Needle deletes it when that member leaves the workspace. Account-sensitive events, such as messages sent from someone else's connected LinkedIn account, are visible only to people with access to that account.

Workarounds:

  • Add production webhooks from a long-lived account that has access to every connected account you need.
  • Subscribe to webhook_deleted and member_removed on another webhook so a deleted webhook doesn't go unnoticed.

How Hookdeck Can Help: Hookdeck's request log shows when Needle traffic stops arriving at the source, which points you at the Needle side quickly.

Paused plans drop events

The Problem: Nothing is delivered for the period a subscription lapsed, even after it is active again.

Why It Happens: Needle pauses webhooks while a plan is inactive, for example after the free trial ends, and doesn't send events from that time later.

Workarounds:

  • Backfill the gap from the Search events API once the plan is active again.

How Hookdeck Can Help: Hookdeck can't recover events that Needle never sent, but its request history makes the gap easy to see and bound.

The secret is shown once

The Problem: You lose the signing secret and can't read it back.

Why It Happens: Needle shows the whsec_ secret only when the webhook is added. Changing the URL, headers or event types keeps the same secret, but there is no way to view it again.

Workarounds:

  • Store the secret in a secrets manager as soon as you add the webhook.
  • If it is lost, delete the webhook and add it again to get a new secret. Undelivered events for the old webhook are dropped.

How Hookdeck Can Help: With Hookdeck in front, only the Hookdeck source holds the Needle secret, so replacing it is one change in one place.

Best practices

Verify against the raw body

The signature covers the exact bytes Needle sent. Use express.raw({ type: '*/*' }) in Express, await request.text() in a Next.js route handler and await request.body() in FastAPI, and parse the JSON only after the check passes.

Ignore event types you don't know

Needle adds event types over time, and "all" subscriptions receive them automatically. Log and acknowledge unknown types with a 2xx rather than failing, so a new type never uses up retries.

Deduplicate on webhook-id

Record webhook-id before processing and skip repeats. See our guide to webhook idempotency.

Acknowledge fast and process asynchronously

Needle allows 10 seconds. Verify, store the event, return 200, and update your CRM or send alerts afterwards. See why to process webhooks asynchronously.

Prefer draft_* over task_held and task_approved

Needle marks task_held and task_approved as deprecated and sends draft_created and draft_approved alongside them. Build approval workflows on the draft_* events, and match the message that follows an approval by data.draftId.

Conclusion

Needle signs every delivery with Standard Webhooks headers and a whsec_ secret you see only once. Verify against the raw body with the decoded key, deduplicate on webhook-id, order by timestamp, and answer within 10 seconds, because Needle stops retrying an hour after the first attempt.

Hookdeck Event Gateway verifies each Needle signature and holds every event until your service has processed it, so a short outage on your side doesn't cost you a prospect's reply.

Get started with Hookdeck for free and handle Needle webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.