Skip to main content
When you create a webhook, you subscribe to one or more of the event types below. Each event is delivered as a JSON POST to your endpoint with a signed header.

Supported events

NON_CAMPAIGN_LEAD_INBOX_MESSAGE_RECEIVED and LEAD_MESSAGE_REPLIED never overlap. A reply from a lead one of your campaigns has already messaged is always LEAD_MESSAGE_REPLIED; everyone else is the inbox event. Subscribe to both to cover every inbound message without receiving anything twice.
Not yet delivered. The subscription API also accepts these event types, but Botdog does not deliver them yet — subscribing to them today will not produce any calls to your endpoint: LEAD_INVITATION_WITHDRAWN, LEAD_SKIPPED, ACCOUNT_DISCONNECTED, DAILY_LIMIT_REACHED. They are reserved for an upcoming release; this page will be updated with their payloads when delivery ships.

Shared fields

Every payload includes the following base fields:

Who the event is about

All lead events (every event except those tagged CAMPAIGN_* in future versions) identify the person twice over — once as you gave them to us, once as LinkedIn shows them today — plus one id that never changes. Both versions are sent because they legitimately differ, and neither alone is enough: yours matches your records but goes stale, LinkedIn’s is current but may no longer match anything you hold. A worked example. You imported Teresa a year ago. Since then she has shortened her display name and changed her custom URL. One webhook about her carries all of this at once:
Nothing is wrong there — it is one person, described two ways.
Which field should I match on?
  • To match a webhook to a record you already hold — use leadLinkedinUrl, or leadName. These are byte-for-byte what you sent us, and Botdog never rewrites them after import, so they always equal your copy.
  • To store a permanent key — use contactLinkedinUrn. It is LinkedIn’s internal member id and never changes, even if they edit their name and URL again tomorrow.
  • To show a person the current truth — use contactName and contactLinkedinUrl.
  • To notice your copy has gone stale — compare contactLinkedinUrl with leadLinkedinUrl. When they differ, it is the same person and your stored URL is out of date.
Avoid matching on contactName or contactLinkedinUrl: they change whenever the person edits their profile, so a match that worked last month can quietly stop working.
Full reference for these five:

Contact details, profile, and your imported lead

Lead events also carry how to reach the person, what LinkedIn says about them, and the lead row you imported:
Empty values. String fields are "" when Botdog does not know the value, never null — the same convention the original contact fields already follow. Numbers and booleans are null when unknown, because 0 and false are real answers here: contactIsPremium: false means “not Premium”, contactIsOpenToWork: null means “we have not been able to check”.
leadCustomAttributes is always a flat string map. Whatever extra columns your CSV had (crm_id, industry, owner…) come back under those exact names, with every value as a string. Nested values are dropped rather than serialised, so a field you map will never arrive as [object Object]. If you store your own CRM record id in a spreadsheet column at import time, it comes straight back here on every event for that lead — the most direct way to match a webhook to a record without matching on names or URLs at all.
contactEnrichedEmail is deliberately separate from contactEmails. contactEmails and contactPhones mean exactly what they always have: contact details published on the LinkedIn profile. Email and phone numbers that Botdog’s enrichment found come back in their own fields instead, so an integration that already trusts contactEmails as LinkedIn-sourced keeps working unchanged. If you want both, read both.
contactLinkedinPublicProfileUrl vs contactLinkedinUrl. contactLinkedinUrl is always present and is built by Botdog from the best identifier it holds (see the note above). contactLinkedinPublicProfileUrl is the URL LinkedIn itself returned on the profile, and is "" until a campaign step has fetched the profile. When both are filled they normally agree.
Profile fields need a profile fetch. contactHeadline, contactLocation, contactProfilePictureUrl, contactFollowerCount, contactIsPremium and contactIsCreator come from the person’s LinkedIn profile, which Botdog reads when a campaign step visits, invites or messages them. In practice they are filled by the time an invitation is accepted. A lead who connected organically with no campaign step ever touching their profile can still arrive with these empty. contactJobTitle, contactLocation and the lead* fields fall back to what you imported, so they are usually filled either way.
Matching a webhook back to your own records. Match on leadLinkedinUrl (or leadName): those are exactly the strings you sent to POST /v1/leads or uploaded in a CSV, and Botdog never rewrites them after import, so they always equal your copy. Use contactLinkedinUrl to notice when someone has changed their custom LinkedIn URL since you imported them — when it differs from leadLinkedinUrl, the person is the same and your copy is stale. Use contactLinkedinUrn as the key you store.
How contactLinkedinUrl is chosen. It is the public identifier Botdog most recently saw on the person’s LinkedIn profile — refreshed every time a campaign step fetches the profile (visit, invitation, message). If Botdog has never fetched the profile it falls back to the slug from the URL you imported, and if it holds nothing public at all — a lead imported by member id or Sales Navigator URL that no step has touched yet, or a sender outside your campaigns — it falls back to https://www.linkedin.com/in/<member id>. That URL opens the profile but will never match a custom URL as text; that is what leadLinkedinUrl and contactLinkedinUrn are for.
NON_CAMPAIGN_LEAD_INBOX_MESSAGE_RECEIVED is the exception: the sender is not a campaign lead, so it carries only the five identity fields above. contactEmails, contactPhones, contactCompanyName and every field in the table below are absent entirely rather than empty, because Botdog holds no enriched profile data for people outside your campaigns. When the sender was never imported, leadName and leadLinkedinUrl are empty strings and contactLinkedinUrl is usually the member-id form — contactLinkedinUrn is then the only reliable way to identify the person.
Per-event fields are documented below.

LEAD_PROFILE_VISITED

Sent when Botdog visits a lead’s profile as part of a campaign step. Extra fields Example payload

LEAD_INVITATION_SENT

Sent when a connection invitation is sent to a lead. Extra fields
On this event specifically, leadInvitedAt is "": the event fires the moment the invitation leaves, just before Botdog records it against the lead. Use sentAt here. Every later event for that lead carries leadInvitedAt normally.
Example payload

LEAD_INVITATION_ACCEPTED

Sent when a lead accepts your connection invitation. Extra fields Example payload

LEAD_MESSAGE_SENT

Sent when an outbound message is delivered to a lead on LinkedIn. Extra fields Example payload

LEAD_MESSAGE_REPLIED

Sent when a lead replies to one of your messages. Extra fields Example payload

NON_CAMPAIGN_LEAD_INBOX_MESSAGE_RECEIVED

Sent when someone outside any campaign sends you an inbound LinkedIn message. That covers:
  • people who message you having never been added to Botdog
  • contacts you imported or added to a list, but never put in a campaign
  • people enrolled in a campaign that has not messaged them yet
It never fires for a lead one of your campaigns has already messaged — that stays LEAD_MESSAGE_REPLIED — so you can subscribe to both without receiving anything twice. Your own outgoing messages never produce this event. Identifying the sender. For the second and third cases there is an imported lead, so leadName and leadLinkedinUrl are filled exactly as for campaign events. For the first case — someone you never imported — there is nothing of yours to return: leadName and leadLinkedinUrl are empty strings, contactName is the LinkedIn display name, contactLinkedinUrl is usually the https://www.linkedin.com/in/<member id> form, and contactLinkedinUrn is the member id itself. Key on contactLinkedinUrn for these. Extra fields
This event carries no campaign, so campaignId and campaignName are empty strings, and it is delivered only to webhooks with scope CONNECTED_ACCOUNT. Campaign scopes (ALL, SPECIFIC_CAMPAIGNS) carry campaign events and never carry this one, so covering both means creating two webhooks — they can point at the same URL.
Expect significantly more volume than the campaign events. This fires for every inbound message from outside your campaigns — recruiter outreach, newsletters, group threads and automated messages, not only conversations you care about. Campaign events are bounded by how much outreach you run; this one is bounded by how much LinkedIn mail you receive.There is no sampling or rate limit on our side, so before enabling it:
  • make sure your endpoint can absorb bursts and returns 2xx quickly
  • filter on your side — contactName and message are usually enough to discard noise
  • remember every delivery is retried on failure, so a slow endpoint compounds the load
Example payload

Field stability

  • Field additions are non-breaking. Botdog may add new fields to existing payloads as features ship. Treat unknown fields as forward-compatible and ignore them.
  • Removals or renames are breaking and will be announced in the Changelog.
  • Test event payloads sent from the dashboard’s Send test event action use the same shape as production deliveries, with stub contact data.