Skip to main content
Webhooks allow you to receive real-time notifications when events happen on your connected accounts — new messages, connection acceptances, incoming connection requests, and more.

Delivery Modes

Webhooks support two delivery modes depending on your setup:

Hosted Mode

No server needed. Events are delivered via SSE stream or polling. Perfect for bots, scripts, and local apps.Create a webhook without a url to use hosted mode.

Custom Mode

Events are sent as POST requests to your server. Requires a publicly accessible HTTPS endpoint.Create a webhook with a url to use custom mode.
Custom mode supports optional HMAC-SHA256 signature verification so your server can reject spoofed payloads. Opt in with enable_signature: true at creation time.

Delivery & retries (custom mode)

Custom webhooks are delivered at least once. Every envelope carries a stable top-level event_id (also sent as X-Linkup-Event-Id, with X-Linkup-Attempt) — dedupe on it. A POST is successful on any 2xx. On timeout (10 s), connection error, 5xx, 408/425/429 we retry with backoff: 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h (8 attempts). Any other 4xx or a 3xx is recorded as failed and not retried. Turn retries off per webhook with retry: false. Everything we sent, or still owe, is visible for 30 days, and replayable, from the API:

How It Works

1

Connect your account

Use POST /v2/login to connect an account and get an account_id.
2

Create a hosted webhook

Call POST /v2/webhooks without a url. You’ll receive a webhook_id, stream_url, and events_url. Monitoring starts automatically.
3

Receive events

Connect to the SSE stream or poll the events endpoint.

Event Types

By default, webhooks listen to all event types. In custom mode, you can filter specific events when creating the webhook by passing an events array. Hosted webhooks (SSE stream and GET /events) always receive every event type — only the message_type direction filter applies — so filter on event.type on your side.
The subscription filter message_received is intentionally different from the actual event.type, which is "message". Filter against message_received when creating the webhook. A LinkedIn message and an inbound email both arrive as event.type === "message" — read the top-level platform field ("linkedin" vs "email") to tell them apart (see Email events).
In addition to subscribed events, every webhook also receives disconnection events — this is not filterable. They fire whenever the underlying platform session expires so you can react (re-auth, alert, etc.).

Event Payload Format

All events share the same wrapper structure. Note that the outer timestamp is when the API emitted the event, while event.timestamp is when the underlying platform activity happened — they differ slightly (latency). The top-level platform field ("linkedin", "email" or "whatsapp") tells you which channel the event came from — this is how you distinguish an inbound email from a LinkedIn DM, since both use event.type: "message". The top-level event_id is the stable identifier of the event: identical on every retry or replay, and the key to dedupe on (see Delivery & retries).

Message Received

Triggered in real-time when a new message is received via the platform’s realtime stream (sub-second latency). The inner event type is "message" and platform is "linkedin".
Field reference
When the message is a reply, an additional event.reply_to object is included with the original message details. For non-reply messages, the field is omitted entirely from the payload — don’t expect a null.
Attachments
type is image, file, audio, video, gif or unknown. Audio and video also carry duration_ms and thumbnail_url.
url is LinkedIn’s own media URL: it requires the account session and its signature expires, so it is not fetchable on its own. Use download_url — see Download Attachment.

Email events

For an email mailbox, the monitor polls the inbox and emits two event types. Both are covered by the message_received filter / all. Email events carry the top-level platform: "email" — that, not event.type, is how you tell them from a LinkedIn DM. New email received — inner type is "message" (same as a LinkedIn message; platform is what distinguishes them):
Sent email bounced — inner type is "email_bounced":
Field reference

Sales Navigator inbox

Sales Navigator has its own inbox, separate from LinkedIn messaging. A reply to an InMail lands there and never appears in the classic thread, so it used to be invisible to webhooks. Both inboxes now feed the same webhook. Use event.inbox to tell them apart. This is automatic: an account whose Sales Navigator seat is connected starts receiving both. Nothing to configure, and no extra credit is charged. An account without a connected seat is unaffected and keeps receiving classic events only.
A prospect can decline an InMail instead of answering it. That comes through as a message event with "message_type": "INMAIL_DECLINE" and an empty message_text. Treat it as a refusal: do not count it as a reply, and stop the sequence.
Identifying the sender The realtime stream only gives the sender’s Sales Navigator id, which is meaningless outside Sales Navigator. We resolve it for you, so the sender arrives in the same shape as on the classic inbox, plus two fields specific to this inbox.
Resolution costs one lookup per contact, not per message, and is cached: the first message from someone takes about half a second longer, their next ones are instant. No credit is charged. If the lookup fails, the event is still delivered with sender.urn_raw alone.
metadata.conversation_urn is a fs_salesMessagingThread URN, not a classic msg_conversation one. Pass its thread id to List Inbox or Get Conversation with sales_nav: true to read the thread, and reply with Send Message using inmail: true.

Accepted Invitation

Triggered when one or more connection requests you sent are accepted. Detected by a background worker that polls the connections list every ~6 hours and diffs against the previously stored snapshot. The payload batches all new connections detected in the same check.
Field reference
When you create a webhook subscribed to accepted_invitation (or all), the API performs an initial sync of your existing connections in the background. The first detection check then runs ~6 hours later and every 6 hours thereafter.

Invitation Received

Triggered when the account receives one or more new connection requests. Detected by the same background worker as accepted_invitation (~6 hour polling cadence by default), so it’s suited for inbound automation — auto-accepting, routing to a CRM, or replying — rather than instant notifications. The payload batches all new invitations detected in the same check.
Field reference
When you create a webhook subscribed to invitation_received (or all), the API stores a baseline of your currently pending invitations in the background — the existing backlog is never notified, only invitations received after the webhook was created.
To auto-accept an incoming invitation, call Accept Invitation with the payload’s entity_urn and shared_secret. invitation_id is delivered as a string because the raw 19-digit number exceeds JavaScript’s Number.MAX_SAFE_INTEGER — keep it as a string end to end.

Disconnection

Triggered when the account’s platform session expires, the cookie is invalidated, or the user logs out. Sent to every webhook attached to the account regardless of subscription filters.
Field reference
When you receive a disconnection event, monitoring stops automatically and every webhook attached to the account is paused (with auto_paused: true). You need to re-authenticate the account via POST /v2/login (or POST /v2/checkpoint if a 2FA is required) — paused webhooks are auto-restarted on a successful re-login.

Architecture

Unlike the V1 webhook system which required creating a separate “webhook account”, the V2 system works with your existing accounts:
  • V2 accounts are created via POST /v2/login (profiles, messages, network, etc.)
  • Webhooks are configurations that attach to those accounts
  • One account can have multiple webhooks with different event filters
  • Starting/stopping monitoring is done per webhook