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.
LEAD_MESSAGE_SENT and INBOX_MESSAGE_SENT never overlap either. A message a campaign step sends is always LEAD_MESSAGE_SENT; a message you type yourself in the Botdog inbox is always INBOX_MESSAGE_SENT, even when the recipient is a campaign lead. Messages sent from LinkedIn directly (the LinkedIn website or app) produce neither.
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:

Whose LinkedIn account did it (team campaigns)

Every lead event (LEAD_PROFILE_VISITED, LEAD_INVITATION_SENT, LEAD_INVITATION_ACCEPTED, LEAD_MESSAGE_SENT, LEAD_MESSAGE_REPLIED) also says whose LinkedIn account did the work: who sent the invitation or message, or received the acceptance or reply. In a team campaign, contacts belong to the teammate who created the campaign (botdogUser*), but each one is contacted from the LinkedIn account of the teammate it’s assigned to (sender*). Use the sender* fields to credit the right person in your CRM. For every other campaign, sender* and botdogUser* name the same user.
Which webhooks receive team campaign events. As well as the campaign creator’s and the team’s webhooks, an event from a team campaign also reaches the all-campaigns webhooks of the teammate whose account did the work, and the specific-campaigns webhooks any teammate created for that campaign. Each webhook receives an event once, however many of these apply.

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 and INBOX_MESSAGE_SENT are the exception: the other person need not be a campaign lead, so they carry 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.
A person in several of your campaigns. One reply produces one delivery per webhook, not one per campaign. A webhook scoped to all campaigns receives it for the campaign whose conversation the reply arrived in (or, if Botdog cannot tell, the campaign that last contacted them). A webhook scoped to specific campaigns receives it for one of the campaigns it selected, so campaignId is always a campaign that webhook subscribed to.
Extra fields Or answer by lead: POST /v1/messages with the leadId sends into the lead’s standard LinkedIn conversation. 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

INBOX_MESSAGE_SENT

Sent when you send a message by hand from the Botdog inbox — replying in a conversation, or starting a new one from the inbox or a contact’s profile. It fires for every recipient, campaign leads included, and only once the message has reached LinkedIn. Messages sent by a campaign step are LEAD_MESSAGE_SENT instead, so you can subscribe to both without receiving anything twice. Messages you send from LinkedIn itself (the website or the mobile app) are not seen by Botdog when you send them and produce no event. Identifying the recipient. If the conversation is linked to one of your leads, leadName and leadLinkedinUrl are filled as for campaign events. Otherwise they are empty strings and the contact fields come from LinkedIn — key on contactLinkedinUrn, as for NON_CAMPAIGN_LEAD_INBOX_MESSAGE_RECEIVED. Extra fields
Like the received event, this carries no campaign (campaignId and campaignName are empty strings) and is delivered only to webhooks with scope CONNECTED_ACCOUNT.
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.