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-levelevent_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
- Hosted Mode
- Custom Mode
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.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 outertimestamp 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 eventtype is "message" and platform is "linkedin".
Attachments
type is image, file, audio, video, gif or unknown. Audio and video also carry duration_ms and thumbnail_url.
Email events
For an email mailbox, the monitor polls the inbox and emits two event types. Both are covered by themessage_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):
type is "email_bounced":
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. Useevent.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.
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.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 asaccepted_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.
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.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.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