Facebook Pixel
Webhooks

What's in the payload

Beginner-friendly walkthrough of the JSON ConnectSafely sends to your webhook URL — every field explained, with an example you can copy and a tip on deduplicating retries.

When something happens, ConnectSafely sends your URL a single POST request:

  • HTTP method: POST
  • Content type: application/json
  • Body: the JSON described below

This page walks through what's in that body so you know exactly what your code is working with.

A real example

Here's what your endpoint will see for a message.received event:

{
  "event": "message.received",
  "object": "event",
  "data": {
    "id": "urn:li:messagingMessage:2-...",
    "message_urn": "urn:li:messagingMessage:2-...",
    "account_id": "65fa...c0",
    "provider": "LINKEDIN",
    "sender": {
      "id": "urn:li:fsd_profile:ACoAA...",
      "name": "Jane Doe",
      "profile_url": null,
      "profile_picture": "https://media.licdn.com/dms/image/..."
    },
    "body": "Hey — saw your post about pipeline automation.",
    "timestamp": "2026-05-12T14:33:21.000Z",
    "conversation_id": "urn:li:messagingThread:2-...",
    "attachments": []
  }
}

Heads up: this is the same shape Unipile uses for message webhooks. Tooling built against Unipile usually works against ConnectSafely with no changes.

The outer wrapper

Every webhook body has the same three top-level fields, regardless of event type:

FieldWhat it is
eventA string saying which kind of thing happened, e.g. message.received
objectAlways the literal string "event". You can ignore it.
dataThe interesting part — fields specific to the event.

So in your code, you'll typically check body.event first, then read body.data.

What's inside data for message.received

FieldPlain English
idUnique ID for this message. Same value as message_urn.
message_urnThe LinkedIn message ID (a URN that starts with urn:li:messagingMessage:). Use this to dedupe.
account_idWhich of your connected ConnectSafely accounts received the message.
providerAlways "LINKEDIN" for now.
sender.idThe sender's LinkedIn profile URN, when LinkedIn gives us one. Can be empty.
sender.nameThe sender's display name, e.g. "Jane Doe".
sender.profile_urlReserved for future use — currently always null.
sender.profile_pictureURL of the sender's avatar, if available.
bodyThe message text itself. Empty string if it had no text body.
timestampWhen LinkedIn says the message was sent, as an ISO 8601 string.
conversation_idThe LinkedIn thread URN — useful if you want to fetch the rest of the conversation later.
attachmentsReserved. Currently always []. We'll populate it when we add attachment support.

InMail and regular DMs both arrive as message.received. If you need to tell them apart, you can check the URN format of message_urn, or call Get conversation messages with conversation_id to look at the thread metadata.

HTTP headers worth knowing

ConnectSafely also sets a few request headers your endpoint can inspect:

HeaderWhy you'd look at it
Content-TypeAlways application/json — your framework will parse the body for you.
User-AgentAlways ConnectSafely-Webhooks/1.0. Useful for allow-listing at a firewall / WAF.
X-Webhook-EventThe same string as body.event. Lets you route by event type without parsing the body first.
X-Webhook-TimestampUnix time (seconds) when ConnectSafely generated the request. Drop very old values to limit replay.
X-Webhook-Signaturesha256=<hex> — the security signature. Check this before trusting the body. How.

If you configured an outbound auth header (Bearer / API key / Basic) when creating the webhook, that header is added too.

"I think I got the same message twice"

That can happen — for example, if your server returns a 500 for one request, ConnectSafely will retry and the message will arrive again. Same message → same message_urn.

Use message_urn as a dedup key:

// Pseudocode — works with any key/value store: Redis, your DB, an in-memory Set
if (await store.has(payload.data.message_urn)) {
  return res.status(200).end(); // already processed
}
await store.add(payload.data.message_urn);
// ... do the real work ...

A 5-minute TTL on the key is usually plenty — retries finish within ~70 seconds.